In the latest PhotoMonkee build I cooked up some rulers to serve as a handy reference point when editing items on the canvas. I’m pretty happy with how they turned out. The feature took a little longer than planned due to some struggles with how I wanted to handle zooming. In the end I took a shortcut (violated some OOP “rules”) to quickly bypass the issues I was hitting.
So what do the new rulers look like?
Well, like rulers! In the above picture you’ll see both a horizontal and vertical ruler extending along the edge of the window. A blue hash mark shows the current position of the cursor. I went with the “quarters” type of ruler whereby major hash marks are shown with three quarter hash marks are drawn in between each major.
Scrolling the canvas (when zoomed in far enough to allow for scrolling) will force a redraw of the ruler to update the coordinates as needed. In addition, zooming the canvas will also force an update to the rulers so that they can account for the new scaling that is required.
Future enhancements I’ve got planned for the rulers include:
- Highlighting an area on the ruler when a selection is made via the marquee tool
- Allowing the user to select the unit type used in the ruler
- Allowing the user to turn the ruler on or off (it’s always on right now)
- Creating guides by dragging them “out” of the rulers (not really a ruler feature but it’ll be involved)
The Design
As with any Qt widget, the ruler widget (PMRuler was the class name I used) will be based off of the QWidget class. Rather than break out the horizontal and vertical drawing logic into separate classes I just created an orientation enum and tossed some conditionals into the drawing routines.
Here is the header file for PMRuler
#define PMRULER_H
#include <QWidget>
namespace Ui {class PMRuler;};
class PMRuler : public QWidget
{
Q_OBJECT
public:
enum Orientation {
Horizontal,
Vertical
};
PMRuler(PMRuler::Orientation orientation, QWidget *parent = 0);
~PMRuler();
QSize sizeHint() const;
QSize minimumSizeHint() const;
void paintEvent(QPaintEvent* pEvent);
void DrawHash(QPainter &painter, QLine &hashLine, int iHashType, int iMinorInterval);
void ZoomUpdate();
private slots:
void MousePosUpdate();
private:
Ui::PMRuler *ui;
PMRuler::Orientation m_Orientation;
QSize m_Size;
QPoint m_MousePos;
QTimer *m_pTimer;
QPixmap m_CachePixmap;
};
#endif // PMRULER_H
What’s The Timer For?
Rather than have the ruler connect to another widget or have the parent window notify the ruler when the mouse moved I opted for polling the moue using QCursor::pos(). Right now I’ve got it set on a 50 millisecond polling interval.
Widget positioning
I found that by using the QGridLayout in the parent window allowed for easy positioning of the ruler widgets and the QScrollArea (which houses the canvas).
QGridLayout *gridLayout = new QGridLayout();
gridLayout->setSpacing(0);
gridLayout->setMargin(0);
QLabel *pLabel = new QLabel("px");
pLabel->setBackgroundRole(QPalette::Window);
pLabel->setStyleSheet("color: rgb(255, 255, 255);");
pLabel->setAlignment(Qt::AlignCenter);
pLabel->setFixedSize(24, 24);
m_pHorizRuler = new PMRuler(PMRuler::Horizontal, this);
m_pHorizRuler->show();
m_pVertRuler = new PMRuler(PMRuler::Vertical, this);
m_pVertRuler->show();
gridLayout->addWidget(pLabel, 0, 0);
gridLayout->addWidget(m_pHorizRuler, 0, 1);
gridLayout->addWidget(m_pVertRuler, 1, 0);
gridLayout->addWidget(m_ScrollArea, 1, 1);
this->setLayout(gridLayout);
Drawing the Ruler
First rule of drawing rulers: cache drawing the ruler as often as possible. Right now we’re drawing major hash marks, quarter hash marks and numbers on the major hash lines. If we add in eighth or sixteenth hash marks things could get a little more expensive. Even without the other hash types, drawing everything each time the mouse moves (to update the blue mouse position indicator) is a huge waste of resources. We only redraw the ruler to a QPixmap cache when the zoom factor is updated or a scroll bar is dragged.
The painting handler
QPainter painter(this);
painter.drawPixmap(0, 0, m_CachePixmap);
QLine mousePosLine;
// Draw the blue hash mark for the current mouse position
if(m_Orientation == Vertical)
mousePosLine = QLine(0, m_MousePos.y(), 24, m_MousePos.y());
else
mousePosLine = QLine(m_MousePos.x(), 0, m_MousePos.x(), 24);
painter.setPen(Qt::blue);
painter.drawLine(mousePosLine);
}//end of PMRuler::paintEvent()
So what is the actual algorithm to draw the ruler? Well, I break it down into two passes: Drawing from 0 to end of the ruler and 0 to the start of the ruler. Each loop simply calls a function to perform drawing where it’s appropriate. The iMinorInterval variable determines the spacing, in real screen coordinates, for each hash marks. This is currently set to 25 pixels which means every 100 pixels we get a major hash mark. The iHashType variable is simply a counter. Inside DrawHash() we do a “ihashType % 4″ check to see if we’re on a major or minor hash.
for(int x = iStart; x < iEnd; x+= iMinorInterval) {
DrawHash(painter, hashLine, iHashType, iMinorInterval);
iHashType++;
}
The hashLine variable (a QLine object) is the current line we’re drawing. It’s a reference so DrawHash() will actually move the line to the next location for the next iteration (woops, did I just violate another OOP rule?)
Coordinate Conversion
During the initial implementation I tried to pass in the scale factor to the PMRuler object and have it convert from actual pixel coordinate space to canvas coordinate space. After a couple of nights of frustration I just punted. The ghetto hack? Have the PMRuler object ask its parent to convert a coordinate from actual pixel space to canvas coordinate space.
You see, when a horizontal PMRuler draws its first major hash mark it is at (100,0). If the canvas is currently set to 100% zoom then we are ALSO at (100,0) on the canvas. If the canvas is currently zoomed in to 110% then (100,0) is actually (110,0) on the canvas.
To perform the conversion I converted the PMRuler‘s local coordinates to global coordinates using mapToGlobal(). Then I take that global coordinate and map it to the QGraphicsView coordinate system using mapFromGlobal(). Lastly we go from widget space to canvas space by calling mapToScene().
Gotchas
Along the way I hit a couple of issues that took some time to resolve. The first was the failure to re-implement the sizeHint() and minimumSizeHint() functions in the PMRuler class. That means when Qt was trying to figure out the appropriate size for the rulers it would end up calling the default QWidget implementation. Who knows what that said the size should be…it definitely wasn’t right!
Readability of the code was also an issue that I took some time to address. Initially I had two methods in PMRuler to perform the drawing. One method was for drawing “forward” (zero to end of ruler) and the other was “backwards” (zero to the start of ruler). I’m glad I took the time to clean things up because while writing this feature. There are other sections of PhotoMonkee that have very messy code and I’m not looking forward to reacquainting myself with them
Conclusion
I’m pretty happy with how the control turned out. It serves as a great basis for some future enhancements and helps PhotoMonkee look a little more like a “real” image editing program. Granted, breaking away from the traditional mold of image editing wouldn’t be a bad thing.
If there are more features you’d like me to write about please drop a comment below or contact me directly: admin at photomonkee dot you know what! Until next time….stay thirsty my friends.
