|  | Home · Overviews · Examples

# Shaped Clock Example

The Shaped Clock example shows how to apply a widget mask to a top-level widget to produce a shaped window.

Widget masks are used to customize the shapes of top-level widgets by restricting the available area for painting. On some window systems, setting certain window flags will cause the window decoration (title bar, window frame, buttons) to be disabled, allowing specially-shaped windows to be created. In this example, we use this feature to create a circular window containing an analog clock.

## ShapedClock Class Implementation

The `ShapedClock` class is based on the `AnalogClock` class defined in the Analog Clock example. The `ShapedClock` constructor performs many of the same tasks as the `AnalogClock` constructor. We set up a timer and connect it to the widget's update() method:

    public class ShapedClock extends QWidget {

        private QPoint dragPosition = new QPoint();
        private static QPolygon hourHand;
        private static QPolygon minuteHand;

        public ShapedClock(QWidget parent) {
            super(parent, new WindowFlags(Qt.WindowType.FramelessWindowHint));
            QTimer timer = new QTimer(this);
            timer.timeout.connect(this, "update()");
            timer.start(1000);

            hourHand = new QPolygon();
            hourHand.append(new QPoint(7, 8));
            hourHand.append(new QPoint(-7, 8));
            hourHand.append(new QPoint(0, -40));

            minuteHand = new QPolygon();
            minuteHand.append(new QPoint(7, 8));
            minuteHand.append(new QPoint(-7, 8));
            minuteHand.append(new QPoint(0, -70));

            setWindowTitle(tr("Shaped Analog Clock"));

            setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu);
            QAction quitAction = new QAction(tr("E&xit"), this);
            quitAction.setShortcut("Ctrl+Q");
            quitAction.triggered.connect(this, "close()");
            addAction(quitAction);

            setToolTip(tr("Drag the clock with the left mouse button.\n" +
                          "Use the right mouse button to open a context menu."));
        }

We inform the window manager that the widget is not to be decorated with a window frame by setting the Qt.WindowType.FramelessWindowHint flag on the widget. As a result, we need to provide a way for the user to move the clock around the screen.

Mouse button events are delivered to the `mousePressEvent()` handler:

        public void mousePressEvent(QMouseEvent event) {
            if (event.button() == MouseButton.LeftButton) {
                QPoint topLeft = frameGeometry().topLeft();
                dragPosition.setX(event.globalPos().x() - topLeft.x());
                dragPosition.setY(event.globalPos().y() - topLeft.y());
                event.accept();
            }
        }

If the left mouse button is pressed over the widget, we record the displacement in global (screen) coordinates between the top-left position of the widget's frame (even when hidden) and the point where the mouse click occurred. This displacement will be used if the user moves the mouse while holding down the left button. Since we acted on the event, we accept it by calling its accept() method.
The `mouseMoveEvent()` handler is called if the mouse is moved over the widget.

        public void mouseMoveEvent(QMouseEvent event) {
            if (event.buttons().isSet(MouseButton.LeftButton)) {
                move(new QPoint(event.globalPos().x() - dragPosition.x(),
                                event.globalPos().y() - dragPosition.y()));
                event.accept();
            }
        }

If the left button is held down while the mouse is moved, the top-left corner of the widget is moved to the point given by subtracting the `dragPosition` from the current cursor position in global coordinates. If we drag the widget, we also accept the event.

The `paintEvent()` method is given for completeness. See the Analog Clock example for a description of the process used to render the clock.

        public void paintEvent(QPaintEvent event) {

            QColor hourColor = new QColor(127, 0, 127);
            QColor minuteColor = new QColor(0, 127, 127, 191);

            int side = Math.min(width(), height());
            QTime time = QTime.currentTime();

            QPainter painter = new QPainter(this);
            painter.setRenderHint(QPainter.RenderHint.Antialiasing);
            painter.translate(width() / 2, height() / 2);
            painter.scale(side / 200.0, side / 200.0);

            painter.setPen(QPen.NoPen);
            painter.setBrush(hourColor);

            painter.save();
            painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0)));
            painter.drawConvexPolygon(hourHand);
            painter.restore();

            painter.setPen(hourColor);

            for (int i = 0; i < 12; ++i) {
                painter.drawLine(88, 0, 96, 0);
                painter.rotate(30.0);
            }

            painter.setPen(QPen.NoPen);
            painter.setBrush(minuteColor);

            painter.save();
            painter.rotate(6.0 * (time.minute() + time.second() / 60.0));
            painter.drawConvexPolygon(minuteHand);
            painter.restore();

            painter.setPen(minuteColor);

            for (int j = 0; j < 60; ++j) {
                if ((j % 5) != 0)
                    painter.drawLine(92, 0, 96, 0);
                painter.rotate(6.0);
            }
        }

In the `resizeEvent()` handler, we re-use some of the code from the `paintEvent()` to determine the region of the widget that is visible to the user:

        public void resizeEvent(QResizeEvent event) {
            int side = Math.min(width(), height());
            QRegion maskedRegion;
            maskedRegion = new QRegion((width() - side) / 2, (height() - side) / 2,
                                        side, side, QRegion.RegionType.Ellipse);
            setMask(maskedRegion);
        }

Since the clock face is a circle drawn in the center of the widget, this is the region we use as the mask.

Although the lack of a window frame may make it difficult for the user to resize the widget on some platforms, it will not necessarily be impossible. The `resizeEvent()` method ensures that the widget mask will always be updated if the widget's dimensions change, and additionally ensures that it will be set up correctly when the widget is first displayed.

To ensure that the widget is given a reasonable default size when it is first shown, we also implement the `sizeHint()` method:

        public QSize sizeHint() {
            return new QSize(100, 100);
        }

Finally, we provide a `main()` method to create and show the shaped clock when the example is run:

        public static void main(String args[]) {
            QApplication.initialize(args);
            ShapedClock shapedClock = new ShapedClock(null);
            shapedClock.show();
            QApplication.exec();
        }

    }

## Notes on Widget Masks

Since QRegion allows arbitrarily complex regions to be created, widget masks can be made to suit the most unconventionally-shaped windows, and even allow widgets to be displayed with holes in them.

Widget masks can also be constructed by using the contents of pixmap to define the opaque part of the widget. For a pixmap with an alpha channel, a suitable mask can be obtained with QPixmap.mask().