瀏覽代碼

Add ctkFlowLayout for dynamic horizontal layouts

Please note that the vertical orientation is broken (due to the lack of
widthForHeight function).
You might want to use preferredExpandingDirections to Qt::Vertical to
encourage a vertical sizeHint even if the orientation of the layout is
horizontal.
Julien Finet 14 年之前
父節點
當前提交
f4f7f70208

+ 3 - 0
Libs/Widgets/CMakeLists.txt

@@ -52,6 +52,8 @@ SET(KIT_SRCS
   ctkFileDialog.h
   ctkFittedTextBrowser.cpp
   ctkFittedTextBrowser.h
+  ctkFlowLayout.cpp
+  ctkFlowLayout.h
   ctkFontButton.cpp
   ctkFontButton.h
   ctkIconEnginePlugin.cpp
@@ -156,6 +158,7 @@ SET(KIT_MOC_SRCS
   ctkDynamicSpacer.h
   ctkFileDialog.h
   ctkFittedTextBrowser.h
+  ctkFlowLayout.h
   ctkFontButton.h
   ctkIconEnginePlugin.h
   ctkMaterialPropertyPreviewLabel.h

+ 2 - 0
Libs/Widgets/Testing/Cpp/CMakeLists.txt

@@ -25,6 +25,7 @@ CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
   ctkDynamicSpacerTest2.cpp
   ctkFileDialogTest1.cpp
   ctkFittedTextBrowserTest1.cpp
+  ctkFlowLayoutTest1.cpp
   ctkFontButtonTest1.cpp
   ctkMaterialPropertyPreviewLabelTest1.cpp
   ctkMaterialPropertyWidgetTest1.cpp
@@ -109,6 +110,7 @@ SIMPLE_TEST( ctkExampleUseOfWorkflowWidgetUsingDerivedSteps )
 SIMPLE_TEST( ctkExampleUseOfWorkflowWidgetUsingSignalsAndSlots )
 SIMPLE_TEST( ctkFileDialogTest1 )
 SIMPLE_TEST( ctkFittedTextBrowserTest1 )
+SIMPLE_TEST( ctkFlowLayoutTest1 )
 SIMPLE_TEST( ctkFontButtonTest1 )
 SIMPLE_TEST( ctkMaterialPropertyPreviewLabelTest1 )
 SIMPLE_TEST( ctkMaterialPropertyWidgetTest1 )

+ 107 - 0
Libs/Widgets/Testing/Cpp/ctkFlowLayoutTest1.cpp

@@ -0,0 +1,107 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QPushButton>
+#include <QSignalSpy>
+#include <QTimer>
+
+// CTK includes
+#include "ctkFlowLayout.h"
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+int ctkFlowLayoutTest1(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  QWidget widget1(0);
+  widget1.setWindowTitle("1) Horizontal");
+  ctkFlowLayout* flowLayout1 = new ctkFlowLayout(&widget1);
+  QPushButton* button11 = new QPushButton("long long long text 1",&widget1);
+  QPushButton* button12 = new QPushButton("super super long long long text 2",&widget1);
+  QPushButton* button13 = new QPushButton("short text 3", &widget1);
+  
+  flowLayout1->addWidget(button11);
+  flowLayout1->addWidget(button12);
+  flowLayout1->addWidget(button13);
+  
+  widget1.setLayout(flowLayout1);
+  widget1.show();
+  
+  QWidget widget2(0);
+  widget2.setWindowTitle("2) Horizontal");
+  ctkFlowLayout* flowLayout2 = new ctkFlowLayout;
+  
+  flowLayout2->addWidget(new QPushButton("one"));
+  flowLayout2->addWidget(new QPushButton("two"));
+  flowLayout2->addWidget(new QPushButton("three"));
+  flowLayout2->addWidget(new QPushButton("four"));
+  flowLayout2->addWidget(new QPushButton("five"));
+  flowLayout2->addWidget(new QPushButton("six"));
+  flowLayout2->addWidget(new QPushButton("seven"));
+  //flowLayout2->setHorizontalSpacing(20);
+  
+  widget2.setLayout(flowLayout2);
+  widget2.show();
+  
+  QWidget widget3(0);
+  widget3.setWindowTitle("3) Vertical");
+  ctkFlowLayout* flowLayout3 = new ctkFlowLayout(Qt::Vertical);
+  
+  flowLayout3->addWidget(new QPushButton("one"));
+  flowLayout3->addWidget(new QPushButton("two"));
+  flowLayout3->addWidget(new QPushButton("three"));
+  flowLayout3->addWidget(new QPushButton("four"));
+  flowLayout3->addWidget(new QPushButton("five"));
+  flowLayout3->addWidget(new QPushButton("six"));
+  flowLayout3->addWidget(new QPushButton("seven"));
+  flowLayout3->setHorizontalSpacing(20);
+  widget3.setLayout(flowLayout3);
+  widget3.show();
+  
+  QWidget widget4(0);
+  widget4.setWindowTitle("4) Horizontal -> Vertical");
+  ctkFlowLayout* flowLayout4 = new ctkFlowLayout();
+  
+  flowLayout4->addWidget(new QPushButton("one"));
+  flowLayout4->addWidget(new QPushButton("two"));
+  flowLayout4->addWidget(new QPushButton("three"));
+  flowLayout4->addWidget(new QPushButton("four"));
+  flowLayout4->addWidget(new QPushButton("five"));
+  flowLayout4->addWidget(new QPushButton("six"));
+  flowLayout4->addWidget(new QPushButton("seven"));
+  
+  widget4.setLayout(flowLayout4);
+  widget4.show();
+  flowLayout4->setOrientation(Qt::Vertical);
+  flowLayout4->setVerticalSpacing(0);
+  flowLayout4->setHorizontalSpacing(0);
+  
+  if (argc < 2 || QString(argv[1]) != "-I")
+    {
+    QTimer::singleShot(200, &app, SLOT(quit()));
+    }
+  return app.exec();
+}

+ 443 - 0
Libs/Widgets/ctkFlowLayout.cpp

@@ -0,0 +1,443 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QDebug>
+#include <QStyle>
+#include <QWidget>
+
+// CTK includes
+#include "ctkFlowLayout.h"
+#include "ctkLogger.h"
+
+static ctkLogger logger("org.commontk.libs.widgets.ctkFlowLayout");
+
+//-----------------------------------------------------------------------------
+class ctkFlowLayoutPrivate
+{
+  Q_DECLARE_PUBLIC(ctkFlowLayout);
+protected:
+  ctkFlowLayout* const q_ptr;
+public:
+  ctkFlowLayoutPrivate(ctkFlowLayout& object);
+  void init();
+  void deleteAll();
+
+  int doLayout(const QRect &rect, bool testOnly) const;
+  int smartSpacing(QStyle::PixelMetric pm) const;
+  QSize maxSizeHint()const;
+
+  QList<QLayoutItem *> ItemList;
+  Qt::Orientation Orientation;
+  int HorizontalSpacing;
+  int VerticalSpacing;
+  bool AlignItems;
+  Qt::Orientations PreferredDirections;
+};
+
+// --------------------------------------------------------------------------
+ctkFlowLayoutPrivate::ctkFlowLayoutPrivate(ctkFlowLayout& object)
+  :q_ptr(&object)
+{
+  this->HorizontalSpacing = -1;
+  this->VerticalSpacing = -1;
+  this->Orientation = Qt::Horizontal;
+  this->PreferredDirections = Qt::Horizontal | Qt::Vertical;
+  this->AlignItems = true;
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayoutPrivate::init()
+{
+  Q_Q(ctkFlowLayout);
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayoutPrivate::deleteAll()
+{
+  Q_Q(ctkFlowLayout);
+  foreach(QLayoutItem* item, this->ItemList)
+    {
+    delete item;
+    }
+  this->ItemList.clear();
+  q->invalidate();
+}
+
+// --------------------------------------------------------------------------
+QSize ctkFlowLayoutPrivate::maxSizeHint()const
+{
+  QSize maxItemSize;
+  foreach (QLayoutItem* item, this->ItemList)
+    {
+    QWidget *wid = item->widget();
+    if (wid && !wid->isVisibleTo(wid->parentWidget()))
+      {// don't take into account hidden items
+      continue;
+      }
+    maxItemSize.rwidth() = qMax(item->sizeHint().width(), maxItemSize.width());
+    maxItemSize.rheight() = qMax(item->sizeHint().height(), maxItemSize.height());
+    }
+  return maxItemSize;
+}
+
+// --------------------------------------------------------------------------
+int ctkFlowLayoutPrivate::doLayout(const QRect& rect, bool testOnly)const
+{
+  Q_Q(const ctkFlowLayout);
+  int left, top, right, bottom;
+  q->getContentsMargins(&left, &top, &right, &bottom);
+  QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
+  QPoint pos = QPoint(effectiveRect.x(), effectiveRect.y());
+  QPoint next = pos;
+  int length = 0;
+  int max = this->Orientation == Qt::Horizontal ?
+    effectiveRect.right() + 1 : effectiveRect.bottom() + 1;
+
+  QSize maxItemSize = this->AlignItems ? this->maxSizeHint() : QSize();
+
+  int spaceX = q->horizontalSpacing();
+  int spaceY = q->verticalSpacing();
+  int space = this->Orientation == Qt::Horizontal ? spaceX : spaceY;
+  foreach (QLayoutItem* item, this->ItemList)
+    {
+    QWidget *wid = item->widget();
+    if (wid && wid->isHidden())
+      {
+      continue;
+      }
+    next = pos;
+    QSize itemSize = this->AlignItems ? maxItemSize : item->sizeHint();
+    if (this->Orientation == Qt::Horizontal)
+      {
+      next += QPoint(itemSize.width() + spaceX, 0);
+      }
+    else
+      {
+      next += QPoint(0, itemSize.height() + spaceY);
+      }
+    if (this->Orientation == Qt::Horizontal &&
+        (next.x() - space > max) && length > 0)
+      {
+      pos = QPoint(effectiveRect.x(), pos.y() + length + space);
+      next = pos + QPoint(itemSize.width() + space, 0);
+      length = 0;
+      }
+    else if (this->Orientation == Qt::Vertical &&
+          (next.y() - space > max) && length > 0)
+      {
+      pos = QPoint( pos.x() + length + space, effectiveRect.y());
+      next = pos + QPoint(0, itemSize.height() + space);
+      length = 0;
+      }
+
+    if (!testOnly)
+      {
+      item->setGeometry(QRect(pos, item->sizeHint()));
+      }
+
+    pos = next;
+    length = qMax(length, this->Orientation == Qt::Horizontal ?
+      itemSize.height() : itemSize.width());
+    }
+  return pos.y() + length - rect.y() + bottom;
+}
+
+//-----------------------------------------------------------------------------
+int ctkFlowLayoutPrivate::smartSpacing(QStyle::PixelMetric pm) const
+{
+  Q_Q(const ctkFlowLayout);
+  QObject* parentObject = q->parent();
+  if (!parentObject)
+    {
+    return -1;
+    }
+  else if (parentObject->isWidgetType())
+    {
+    QWidget* parentWidget = qobject_cast<QWidget *>(parentObject);
+    return parentWidget->style()->pixelMetric(pm, 0, parentWidget);
+    }
+  else
+    {
+    return static_cast<QLayout *>(parentObject)->spacing();
+    }
+}
+
+// --------------------------------------------------------------------------
+ctkFlowLayout::ctkFlowLayout(Qt::Orientation orientation, QWidget *parentWidget)
+  : Superclass(parentWidget)
+  , d_ptr(new ctkFlowLayoutPrivate(*this))
+{
+  Q_D(ctkFlowLayout);
+  d->init();
+  this->setOrientation(orientation);
+}
+
+// --------------------------------------------------------------------------
+ctkFlowLayout::ctkFlowLayout(QWidget *parentWidget)
+  : Superclass(parentWidget)
+  , d_ptr(new ctkFlowLayoutPrivate(*this))
+{
+  Q_D(ctkFlowLayout);
+  d->init();
+}
+
+// --------------------------------------------------------------------------
+ctkFlowLayout::ctkFlowLayout()
+  : d_ptr(new ctkFlowLayoutPrivate(*this))
+{
+  Q_D(ctkFlowLayout);
+  d->init();
+}
+
+// --------------------------------------------------------------------------
+ctkFlowLayout::~ctkFlowLayout()
+{
+  Q_D(ctkFlowLayout);
+  d->deleteAll();
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::setOrientation(Qt::Orientation orientation)
+{
+  Q_D(ctkFlowLayout);
+  d->Orientation = orientation;
+  this->invalidate();
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::setPreferredExpandingDirections(Qt::Orientations directions)
+{
+  Q_D(ctkFlowLayout);
+  d->PreferredDirections = directions;
+}
+
+// --------------------------------------------------------------------------
+Qt::Orientations ctkFlowLayout::preferredExpandingDirections()const
+{
+  Q_D(const ctkFlowLayout);
+  return d->PreferredDirections;
+}
+  
+// --------------------------------------------------------------------------
+Qt::Orientation ctkFlowLayout::orientation() const
+{
+  Q_D(const ctkFlowLayout);
+  return d->Orientation;
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::setHorizontalSpacing(int spacing)
+{
+  Q_D(ctkFlowLayout);
+  d->HorizontalSpacing = spacing;
+  this->invalidate();
+}
+
+// --------------------------------------------------------------------------
+int ctkFlowLayout::horizontalSpacing() const
+{
+  Q_D(const ctkFlowLayout);
+  if (d->HorizontalSpacing < 0)
+    {
+    return d->smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
+    }
+  return d->HorizontalSpacing;
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::setVerticalSpacing(int spacing)
+{
+  Q_D(ctkFlowLayout);
+  d->VerticalSpacing = spacing;
+  this->invalidate();
+}
+
+// --------------------------------------------------------------------------
+int ctkFlowLayout::verticalSpacing() const
+{
+  Q_D(const ctkFlowLayout);
+  if (d->VerticalSpacing < 0)
+    {
+    return d->smartSpacing(QStyle::PM_LayoutVerticalSpacing);
+    }
+  return d->VerticalSpacing;
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::setAlignItems(bool align)
+{
+  Q_D(ctkFlowLayout);
+  d->AlignItems = align;
+  this->invalidate();
+}
+
+// --------------------------------------------------------------------------
+bool ctkFlowLayout::alignItems() const
+{
+  Q_D(const ctkFlowLayout);
+  return d->AlignItems;
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::addItem(QLayoutItem *item)
+{
+  Q_D(ctkFlowLayout);
+  d->ItemList << item;
+  this->invalidate();
+}
+
+// --------------------------------------------------------------------------
+Qt::Orientations ctkFlowLayout::expandingDirections() const
+{
+  Q_D(const ctkFlowLayout);
+  //return d->Orientation;
+  return Qt::Vertical | Qt::Horizontal;
+}
+
+// --------------------------------------------------------------------------
+bool ctkFlowLayout::hasHeightForWidth() const
+{
+  return true;
+}
+
+// --------------------------------------------------------------------------
+int ctkFlowLayout::heightForWidth(int width) const
+{
+  Q_D(const ctkFlowLayout);
+  /// here we see the limitations of the vertical layout, it should be
+  /// widthForHeight in this case.
+  int height = d->doLayout(QRect(0, 0, width, 0), true);
+  return height;
+}
+
+// --------------------------------------------------------------------------
+int ctkFlowLayout::count() const
+{
+  Q_D(const ctkFlowLayout);
+  return d->ItemList.count();
+}
+
+// --------------------------------------------------------------------------
+QLayoutItem *ctkFlowLayout::itemAt(int index) const
+{
+  Q_D(const ctkFlowLayout);
+  if (index < 0 || index >= this->count())
+    {
+    return 0;
+    }
+  return d->ItemList[index];
+}
+
+// --------------------------------------------------------------------------
+QSize ctkFlowLayout::minimumSize() const
+{
+  Q_D(const ctkFlowLayout);
+  QSize size;
+  foreach(QLayoutItem* item, d->ItemList)
+    {
+    QWidget* widget = item->widget();
+    if (widget && !widget->isVisibleTo(widget->parentWidget()))
+      {
+      continue;
+      }
+    size = size.expandedTo(item->minimumSize());
+    }
+  int left, top, right, bottom;
+  this->getContentsMargins(&left, &top, &right, &bottom);
+  size += QSize(left+right, top+bottom);
+  return size;
+}
+
+// --------------------------------------------------------------------------
+void ctkFlowLayout::setGeometry(const QRect &rect)
+{
+  Q_D(ctkFlowLayout);
+  this->QLayout::setGeometry(rect);
+  d->doLayout(rect, false);
+}
+
+// --------------------------------------------------------------------------
+QSize ctkFlowLayout::sizeHint() const
+{
+  Q_D(const ctkFlowLayout);
+  QSize size = QSize(0,0);
+  int countX = 0;
+  int countY = 0;
+  QSize maxSizeHint = d->AlignItems ? d->maxSizeHint() : QSize();
+  // Add items
+  foreach (QLayoutItem* item, d->ItemList)
+    {
+    QWidget* widget = item->widget();
+    if (widget && !widget->isVisibleTo(widget->parentWidget()))
+      {
+      continue;
+      }
+    QSize itemSize = d->AlignItems ? maxSizeHint : item->sizeHint();
+    Qt::Orientation grow;
+    if (d->PreferredDirections & Qt::Horizontal &&
+        !(d->PreferredDirections & Qt::Vertical))
+      {
+      grow = Qt::Horizontal;
+      }
+    else if (d->PreferredDirections & Qt::Vertical &&
+             !(d->PreferredDirections & Qt::Horizontal))
+      {
+      grow = Qt::Vertical;
+      }
+    else
+      {
+      grow = countY >= countX ? Qt::Horizontal : Qt::Vertical;
+      }
+    if (grow == Qt::Horizontal)
+      {
+      size.rwidth() += itemSize.width();
+      size.rheight() = qMax(itemSize.height(), size.height());
+      ++countX;
+      }
+    else
+      {
+      size.rwidth() = qMax(itemSize.width(), size.width());
+      size.rheight() += itemSize.height();
+      ++countY;
+      }
+    }
+  // Add spacing
+  size += QSize((countX-1) * this->horizontalSpacing(),
+                (countY-1) * this->verticalSpacing());
+  // Add margins
+  int left, top, right, bottom;
+  this->getContentsMargins(&left, &top, &right, &bottom);
+  size += QSize(left+right, top+bottom);
+  return size;
+}
+
+// --------------------------------------------------------------------------
+QLayoutItem *ctkFlowLayout::takeAt(int index)
+{
+  Q_D(ctkFlowLayout);
+  if (index < 0 || index >= this->count())
+    {
+    return 0;
+    }
+  QLayoutItem* item = d->ItemList.takeAt(index);
+  this->invalidate();
+  return item;
+}

+ 82 - 0
Libs/Widgets/ctkFlowLayout.h

@@ -0,0 +1,82 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkFlowLayout_h
+#define __ctkFlowLayout_h
+
+// Qt includes
+#include <QLayout>
+
+// CTK includes
+#include "ctkWidgetsExport.h"
+class ctkFlowLayoutPrivate;
+
+/// Warning the Vertical orientation is NOT fully supported. You can obtain
+/// strange behavior
+class CTK_WIDGETS_EXPORT ctkFlowLayout : public QLayout
+{
+  Q_OBJECT
+  Q_PROPERTY(Qt::Orientation orientation READ orientation WRITE setOrientation)
+  Q_PROPERTY(int horizontalSpacing READ horizontalSpacing WRITE setHorizontalSpacing)
+  Q_PROPERTY(int verticalSpacing READ verticalSpacing WRITE setVerticalSpacing)
+  Q_PROPERTY(bool alignItems READ alignItems WRITE setAlignItems)
+  Q_PROPERTY(Qt::Orientations preferredExpandingDirections READ preferredExpandingDirections WRITE setPreferredExpandingDirections)
+public:
+  typedef QLayout Superclass;
+  explicit ctkFlowLayout(Qt::Orientation orientation, QWidget* parent = 0);
+  explicit ctkFlowLayout(QWidget* parent);
+  explicit ctkFlowLayout();
+  virtual ~ctkFlowLayout();
+  
+  void setOrientation(Qt::Orientation orientation);
+  Qt::Orientation orientation()const;
+  
+  void setPreferredExpandingDirections(Qt::Orientations directions);
+  Qt::Orientations preferredExpandingDirections()const;
+
+  int horizontalSpacing() const;
+  void setHorizontalSpacing(int);
+
+  int verticalSpacing() const;
+  void setVerticalSpacing(int);
+  
+  bool alignItems()const;
+  void setAlignItems(bool);
+
+  virtual void addItem(QLayoutItem *item);
+  virtual Qt::Orientations expandingDirections() const;
+  virtual bool hasHeightForWidth() const;
+  virtual int heightForWidth(int) const;
+  virtual int count() const;
+  virtual QLayoutItem *itemAt(int index) const;
+  virtual QSize minimumSize() const;
+  virtual void setGeometry(const QRect &rect);
+  virtual QSize sizeHint() const;
+  virtual QLayoutItem *takeAt(int index);
+  
+protected:
+  QScopedPointer<ctkFlowLayoutPrivate> d_ptr;
+
+private:
+  Q_DECLARE_PRIVATE(ctkFlowLayout);
+  Q_DISABLE_COPY(ctkFlowLayout);
+};
+
+#endif