My blog has been moved to ariya.ofilabs.com.

Wednesday, October 26, 2005

Improving QSplitter with Quantized Splitting Step

QSplitter is undoubtly quite a very useful widget. Dragging the handle allows the user to easily resize other widgets. This screenshot shows how the splitter resizes a multiline editor QTextEdit:

The font used in the editor is made deliberately large. Why? This is to show that with normal QSplitter, you may drag the mouse and move the rubber band anywhere. So you may end up resizing the editor widget right in the middle of a line, like shown above. The third line would only be half visible, or half hidden.

Wouldn't it be nice if we restrict the splitter a bit so that it won't "cut" any line? Like for example in the screenshot below. Now the rubberband is either below the fourth line or above it, it will never be in the middle of the line.

Is it possible? Of course. Fortunately Qt offers an easy way to do this. Note that I'm talking about Qt 4 here, it will be a different workaround for Qt 3.

The trick is to use a customized splitter and its handle.

So first derive a new class from QSplitter, e.g. CustomSplitter and override the createHandle method, as easy as:

QSplitterHandle* CustomSplitter::createHandle()
{
  return new CustomSplitterHandle( this );
}

Of course you now need to create class CustomSplitterHandle, derived from the standard QSplitterHandle. In this class, we need to reimplement two mouse event handlers: mouseMoveEvent and mouseReleaseEvent.

The first one should look like:

void CustomSplitterHandle::mouseMoveEvent(QMouseEvent *e)
{
  if( !(e->buttons() & Qt::LeftButton) )
    return;
    
  QFont font("Arial",20)  ;
  QFontMetrics fm( font );
  int fh = fm.height();
  int margin = 2;
    
  int pos = parentWidget()->mapFromGlobal(e->globalPos()).y();
  pos = pos - margin;
  pos = (int)(pos/fh)*fh;
  pos = pos + margin;
  splitter->setRubberBand( closestLegalPosition(pos) );
}

For illustration purpose, I hardcode the splitter step as the height of Arial, 20pt font. And I also only consider the case where the splitter's orientation is vertical and it is in opaque sizing mode. As you can see, variable pos here will only be an integer multiple of the step, thereby giving a quantized step for the handle movement.

Moving the rubber band itself does not resize the widget. So mouseReleaseEvent needs to be implemented as well, like shown herebelow. Note the much duplicate code...

void CustomSplitterHandle::mouseReleaseEvent(QMouseEvent *e)
{
  splitter->setRubberBand(-1);

  QFont font("Arial",20)  ;
  QFontMetrics fm( font );
  int fh = fm.height();
  int margin = 2;
    
  int pos = parentWidget()->mapFromGlobal(e->globalPos()).y();
  pos = pos - margin;
  pos = (int)(pos/fh)*fh;
  pos = pos + margin;

  moveSplitter( pos );
}

Don't forget to make class CustomSplitterHandle as a friend of CustomSplitter. If they are not in good friendship, moveSplitter will not work.

When can this trick be useful? At least, in a spreadsheet application. Seasoned Excel users know that splitting a worksheet into two (and into four) parts exactly works in this way. Excel won't divide the view halfway within one column, the splitter is always positioned between two columns. Also, in the upcoming Excel 12, user can resize the formula bar so it will not be a single line anymore. I bet that you can't make it one-and-half line.

No comments: