8.2. Dialog Layout the Simple Way

When you have decided what components are needed in the dialog to accomplish the intended task, place them in such a way that the usage is intuitive for the end user. Any widget can be placed in a dialog by defining its x and y coordinates with respect to the upper-left corner of the parent widget, along with the width and height. This must be repeated for each and every widget in the dialog. In theory, this is straightforward, but in practical life several complicating factors exist:

  1. What to do when a dialog is resized? Which widgets should stretch and how much, and which remain fixed in size?

  2. How much work is required when you suddenly need to add one or more widgets or perhaps remove another? This may require completely new layout code and can take considerable time to finish for complex dialogs.

  3. What extra complexity do you have to add to your code to handle font and font-size changes? Remember that the users may prefer another font and/or font-size than you.

  4. How easy is it, in general, to support label strings and text that have no predictable size? This is the case for KDE applications that need to support multiple languages.

The remedy to all these problems is to use the QLayout classes to manage the widgets. The widgets' size, stretchability, and position with respect to the others are easily controlled this way. Listing 8.2 contains the same constructor using old-style manual placement (OldDialog) and the QLayout-based (NewDialog). The difference should easily convince you to use the QLayout classes and the KDialogBase widget class to manage the widgets, because you no longer have to use the setGeometry() calls. What would you have to do in OldDialog if the string length of the first QLabel got longer or another font size should be used? Try to add a new label below the first one in the OldDialog. Figure 8.2 shows what the dialog looks like when it is based on QLayouts.


Example 8.2. The Difference Between the Old Style (Manual) Geometry Strategy and the New Based on QLayouts

   1 
   2 1: OldDialog::OldDialog( QWidget *parent, const char *name, bool modal )
   3 2:   : QDialog( parent, name, modal )
   4 3: {
   5 4:   setCaption( i18n("Update Frequency") );
   6 5:
   7 6:   QLabel *label = new QLabel( this, "label" );
   8 7:   label->setGeometry( 16, 24, 180, 24 );
   9 8:   label->setText( i18n("Update frequency in seconds:") );
  10 9:
  11 10:   QScrollBar *scrollbar = new QScrollBar( this, "scrollbar" );
  12 11:   scrollbar->setGeometry( 24, 48, 160, 16 );
  13 12:   scrollbar->setOrientation( QScrollBar::Horizontal );
  14 13:
  15 14:   QLCDNumber *lcdNumber = QLCDNumber( this, "lcdnumber" );
  16 15:   lcdNumber->setGeometry( 192, 32, 72, 32 );
  17 16:   lcdNumber->setSmallDecimalPoint( false );
  18 17:   lcdNumber->setNumDigits( 5 );
  19 18:   lcdNumber->setMode( QLCDNumber::DEC );
  20 19:
  21 20:   QPushButton *okPushButton = new QPushButton( this, "ok" );
  22 21:   okPushButton->setGeometry( 32, 120, 80, 24 );
  23 22:   okPushButton->setText( i18n("&OK") );
  24 23:
  25 24:   QPushButton *cancelPushButton = new QPushButton( this, "PushButton_2" );
  26 25:   cancelPushButton->setGeometry( 176, 120, 80, 24 );
  27 26:   cancelPushButton->setText( i18n("&Cancel") );
  28 27:
  29 28:   resize( 288, 168 );
  30 29:
  31 30:   connect( scrollbar, SIGNAL(valueChanged(int)),
  32 31:      lcdNumber, SLOT(display(int)) );
  33 32:   connect( okPushButton, SIGNAL(clicked()), this, SLOT(accept()) );
  34 33:   connect( cancelPushButton, SIGNAL(clicked()), this, SLOT(reject()) );
  35 34: }
  36 35:
  37 36: NewDialog::NewDialog( QWidget *parent, const char* name, bool modal )
  38 37:   : KDialogBase( parent, name, modal, i18n("Update Frequency"),
  39 38:      Ok|Cancel, Ok )
  40 39: {
  41 40:   QWidget *page = new QWidget( this );
  42 41:   setMainWidget(page);
  43 42:
  44 43:   QVBoxLayout *topLayout = new QVBoxLayout( page, 0, spacingHint() );
  45 44:   QHBoxLayout *hlay = new QHBoxLayout( topLayout );
  46 45:   QVBoxLayout *vlay = new QVBoxLayout( hlay, 10 );
  47 46:
  48 47:   QLabel *label2 = new QLabel( page, "label1" );
  49 48:   label->setText( i18n("Update frequency in seconds:") );
  50 49:   vlay->addWidget( label );
  51 50:
  52 51:   //
  53 52:   // It is very simple to add a new label here.
  54 53:   //
  55 54:   //QLabel *label2 = new QLabel( page, "label2" );
  56 55:   //label2->setText( i18n("A new label text") );
  57 56:   //vlay->addWidget( label2 );
  58 57:
  59 58:   QScrollBar *scrollbar = QScrollBar( page, "scrollbar" );
  60 59:   scrollbar->setOrientation( QScrollBar::Horizontal );
  61 60:   vlay->addWidget( scrollbar );
  62 61:
  63 62:   QLCDNumber *lcdNumber = QLCDNumber( page, "lcdnumber" );
  64 63:   lcdNumber->setSmallDecimalPoint( false );
  65 64:   lcdNumber->setNumDigits( 3 );
  66 65:   lcdNumber->setMode( QLCDNumber::DEC );
  67 66:   hlay->addWidget( lcdNumber, 0 );
  68 67:
  69 68:   connect( scrollbar, SIGNAL(valueChanged(int)),
  70 69:      lcdNumber, SLOT(display(int)) );
  71 70: }
  72 


Figure 8.2. The appearance of the dialog that is implemented in the second constructor of Listing 8.2.


A QLayout can be vertically oriented (QVBoxLayout), horizontally oriented (QHBoxLayout) or be a grid layout (QGridLayout). The Qt layout mechanism is to some extent described in Chapter 4, "Creating Custom KDE Widgets." Therefore, in the following section, some problems that often appear when writing dialogs are described.

Note

You can also define your own custom layout managers, but that is outside the scope of this chapter.

The first important thing you must know is that there can be only one layout manager per widget. This constraint does not prevent you from nesting layouts. As Listing 8.2 illustrates, a layout can be inserted into a parent layout, thus becoming a child layout. On line 43, topLayout is the parent of the horizontal layout hlay, which, in turn, is the parent of the vertical layout vlay.

The second constraint you must know is that a widget that is managed by a layout (a parent or a child layout) must be a child widget of the same widget that contains the layout manager. This can been seen in Listing 8.2 on lines 58–60, where scrollbar is a child of page and is managed by vlay, which is a grandchild layout of topLayout.

A widget that is managed by a layout manager can itself contain its own layout manager. Thus you can create a hierarchy of layouts and widgets of any desired complexity. Listing 8.3 shows how the Goto dialog of KHexEdit is using a frame with a title that groups the toggle buttons. The group widget is managed by the topLayout manager and contains the gbox layout. Figure 8.3 shows the appearance of this dialog.


Example 8.3. The CGotoDialog Class Uses the KDialogBase Class Somewhat Differently from What Has Been Shown Earlier

   1 
   2 1:  class CGotoDialog : public KDialogBase
   3 2: {
   4 3:   Q_OBJECT
   5 4:
   6 5:   public:
   7 6:     CGotoDialog( QWidget *parent=0, const char *name=0,
   8 7:                  bool modal=false );
   9 8:     ~CGotoDialog();
  10 9:     void defaultFocus();
  11 10:
  12 11:   protected slots:
  13 12:     virtual void slotOk();
  14 13:
  15 14:   signals:
  16 15:     void gotoOffset( uint offset, uint bit, bool fromCursor,
  17 16:                      bool forward );
  18 17:
  19 18:   private:
  20 19:     QComboBox *mComboBox;
  21 20:     QCheckBox *mCheckBackward;
  22 21:     QCheckBox *mCheckFromCursor;
  23 22:     QCheckBox *mCheckVisible;
  24 23: };
  25 24:
  26 25: CGotoDialog::CGotoDialog( QWidget *parent, const char *name, bool modal )
  27 26:   :KDialogBase( Plain, i18n("Goto Offset"), Ok|Cancel, Ok, parent, name,
  28 27:     modal )
  29 28: {
  30 29:   QVBoxLayout *topLayout = new QVBoxLayout( plainPage(), 0,
  31 30:                                             spacingHint() );
  32 31:   CHECK_PTR(topLayout);
  33 32: 
  34 33:   QVBoxLayout *vbox = new QVBoxLayout( topLayout );
  35 34:   CHECK_PTR(vbox);
  36 35:
  37 36:   mComboBox = new QComboBox( true, plainPage() );
  38 37:   CHECK_PTR(mComboBox);
  39 38:   mComboBox->setMaxCount( 10 );
  40 39:   mComboBox->setInsertionPolicy( QComboBox::AtTop );
  41 40:   mComboBox->setMinimumWidth( fontMetrics().maxWidth()*17 );
  42 41:
  43 42:   QLabel *label = new QLabel( mComboBox, i18n("O&ffset:"), plainPage() );
  44 43:   CHECK_PTR(label);
  45 44:
  46 45:   vbox->addWidget( label );
  47 46:   vbox->addWidget( mComboBox );
  48 47:
  49 48:   QButtonGroup *group = new QButtonGroup( i18n("Options"), plainPage() );
  50 49:   CHECK_PTR(group);
  51 50:   topLayout->addWidget( group, 10 ); // Only the group will be resized
  52 51:
  53 52:   QGridLayout *gbox = new QGridLayout( group, 4, 2, spacingHint() );
  54 53:   CHECK_PTR(gbox);
  55 54:   gbox->addRowSpacing( 0, fontMetrics().lineSpacing() );
  56 55:   mCheckFromCursor = new QCheckBox( i18n("&From cursor"), group );
  57 56:   CHECK_PTR(mCheckFromCursor);
  58 57:   gbox->addWidget( mCheckFromCursor, 1, 0 );
  59 58:   mCheckBackward = new QCheckBox( i18n("&Backwards"), group );
  60 59:   CHECK_PTR(mCheckBackward);
  61 60:   gbox->addWidget( mCheckBackward, 1, 1 );
  62 61:   mCheckVisible = new QCheckBox( i18n("&Stay visible"), group );
  63 62:   CHECK_PTR(mCheckVisible);
  64 63:   gbox->addWidget( mCheckVisible, 2, 0 );
  65 64:   gbox->setRowStretch( 3, 10 ); // Eat up all extra space when resized.
  66 65:   mCheckVisible->setChecked( true );
  67 66:
  68 67:   defaultFocus();
  69 68: }
  70 69:
  71 70: CGotoDialog::~CGotoDialog()
  72 71: {
  73 72: }
  74 73:
  75 74: void
  76 75: CGotoDialog::defaultFocus()
  77 76: {
  78 77:   mComboBox->setFocus();
  79 78: }
  80 79:
  81 80: void
  82 81: CGotoDialog::slotOk()
  83 82: {
  84 83:   uint offset;
  85 84:   bool success = stringToOffset( mComboBox->currentText(), offset );
  86 85:   if( success == false )
  87 86:   {
  88 87:     return;
  89 88:   }
  90 89:
  91 90:   if( mCheckVisible->isChecked() == false )
  92 91:   {
  93 92:     hide();
  94 93:   }
  95 94:   emit gotoOffset( offset, 7, mCheckFromCursor->isChecked(),
  96 95:        mCheckBackward->isChecked() == true ? false : true );
  97 96: }
  98 97:
  99 98: // The dialog used as the main application window
 100 99: #include <kcmdlineargs.h>
 101 100: int main( int argc, char **argv )
 102 101: {
 103 102:   KCmdLineArgs::init(argc, argv, "khexedit", 0, 0);
 104 103:   KApplication app;
 105 104:   CGotoDialog *dialog = new CGotoDialog;
 106 105:   dialog->show();
 107 106:   int result = app.exec();
 108 107:   return result;
 109 108: }
 110 

As can be seen from Listing 8.3, the Plain mode in the constructor instructs KDialogBase to create a main widget by itself. This widget is returned by plainPage() and serves as the parent widget for all layouts and other widgets. A little—but important—trick is used in Listing 8.3 as well. On line 54, notice the fontMetrics().lineSpacing(). It reserves space so that the uppermost child widget (the From Cursor toggle button) of the frame does not obscure the title string. Never make this spacing by using a fixed integer value. It will work with the font that you use, but when the users of your application change the font size, it will break. The fontMetrics().lineSpacing() returns a value that depends on the font. This seems to be a missing feature in the Qt library code, so future versions of the Qt library may not require this workaround.


Figure 8.3. The Goto dialog of Listing 8.3.


The method of first creating a widget and then the layout for each and every widget that needs it can be cumbersome if it has to be repeated for many widgets. To simplify this, the Qt library contains two widgets, QVBox and QHBox, which create the layout manager internally. These are intended for simple layouts. The widget children of a QVBox widget are placed vertically, and the children of QHBox widget are placed next to each other. A QVBox widget itself can, of course, be managed by a layout. Listing 8.4 shows the constructor of Listing 8.3 but uses a QVBox instead of a layout. You make only one layout yourself in this example.


Example 8.4. The CGotoDialog Class Using a QVBox Widget to Do the Geometry Management

   1 
   2 1: CGotoDialog::CGotoDialog( QWidget *parent, const char *name, bool modal )
   3 2:   :KDialogBase( Plain, i18n("Goto Offset"), Ok|Cancel, Ok, parent, name,
   4 3:         modal )
   5 4: {
   6 5:    QVBoxLayout *topLayout = new QVBoxLayout(plainPage(), 0, spacingHint());
   7 6:    CHECK_PTR(topLayout);
   8 7:
   9 8:    QVBox *topBox = new QVBox( plainPage() );
  10 9:    CHECK_PTR(topBox);
  11 10:   topBox->setSpacing( spacingHint() );
  12 11:   topLayout->addWidget( topBox );
  13 12:
  14 13:   QLabel *label = new QLabel( i18n("O&ffset:"), topBox );
  15 14:   CHECK_PTR(label);
  16 15:
  17 16:   mComboBox = new QComboBox( true, topBox );
  18 17:   CHECK_PTR(mComboBox);
  19 18:   mComboBox->setMaxCount( 10 );
  20 19:   mComboBox->setInsertionPolicy( QComboBox::AtTop );
  21 20:   mComboBox->setMinimumWidth( fontMetrics().maxWidth()*17 );
  22 21:   label->setBuddy(mComboBox); // To get the underlining to work
  23 22:
  24 23:   QButtonGroup *group = new QButtonGroup( i18n("Options"), topBox );
  25 24:   CHECK_PTR(group);
  26 25:
  27 26:   QGridLayout *gbox = new QGridLayout( group, 4, 2, spacingHint() );
  28 27:   CHECK_PTR(gbox);
  29 28:   gbox->addRowSpacing( 0, fontMetrics().lineSpacing() );
  30 29:   mCheckFromCursor = new QCheckBox( i18n("&From cursor"), group );
  31 30:   CHECK_PTR(mCheckFromCursor);
  32 31:   gbox->addWidget( mCheckFromCursor, 1, 0 );
  33 32:   mCheckBackward = new QCheckBox( i18n("&Backwards"), group );
  34 33:   CHECK_PTR(mCheckBackward);
  35 34:   gbox->addWidget( mCheckBackward, 1, 1 );
  36 35:   mCheckVisible = new QCheckBox( i18n("&Stay visible"), group );
  37 36:   CHECK_PTR(mCheckVisible);
  38 37:   gbox->addWidget( mCheckVisible, 2, 0 );
  39 38:   gbox->setRowStretch( 3, 10 ); // Eat up all extra space when resized.
  40 39:   mCheckVisible->setChecked( true );
  41 40:
  42 41:   defaultFocus();
  43 42: }
  44 

You should keep several design issues in mind when using QLayouts. The following list can be considered a checklist:

  1. What spacing and margin values do you use in the QLayouts? Make sure you use the same values for spacing and margins for all dialogs you make, because this makes the overall appearance much better. Never use hard-coded values. Define constants in a common header file and use them without exception. As indicated in Listing 8.3, dialogs derived from KDialogBase have access to a spacingHint() and (not shown) a marginHint() method. These provide the values you need. Note: Normally you can ignore the marginHint() because this is reserved for the space between the dialog edge and the outermost widget. The KDialogBase takes care of setting up this space internally.

  2. Should your dialog be resizable? In most cases, there is no reason to not allow resizing. This means that a dialog can be made larger than the default minimum size, but never smaller. The default minimum size is automatically computed by the QLayouts just before the dialog becomes visible. A dialog that contains editable fields or lists should always be resizable, whereas dialogs that contain widgets that require a long time to resize should perhaps be fixed. The KDialogBase class contains one method, disableResize(), which prevents the dialog from being resized. It must be called just before show() or exec(). The default behavior of KDialogBase is to allow resizing.

  3. Which widgets in your dialog are stretchable? When you add a widget to a layout with addWidget(…), you can also specify a stretch factor. The widget with the biggest stretch factor is resized the most when the layout is resized. Normally, list widgets and multiline edit widgets should be at least stretchable vertically, and anything containing an edit field should be horizontally stretchable. You can also set an empty space to be stretchable. This is done in Listing 8.4 on line 38.

  4. How does your dialog look when you change the length of the strings? Always test with both short and long strings for the various labels and so on. You will then locate potential problems long before a translation is being made or before a modification of an original string is made.