Recently, the project required implementing a rubber band affect on Windows, so I did some research and want to share it.
Windows system supports the rubber band effect, which can be achieved using Win32 methods: SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, showFullWindow, NULL, 0); showFullWindow is a variable. If you need the default Windows rubber band effect, pass false, othrewise pass true. When using the default rubber band resizing, a rectangular frame appears, which affects both window movement and resizing. When the mouse is released, the real window moves or resizes. If not using the rubber band drag method, the window will move in real-time.
Figure 1: Windows Rubber Band
When using Qt windows, if you want to support this drag method on Windows, you cannot use setGeometry to move or resize the window. Instead, you need to override QWidget::nativeEvent, which is called before messages enter the Qt event loop. This means that nativeEvent is called before mouseEvent methods. For the implementation of nativeEvent, please refer to another article titled "Qt Drag and Resize." However, during my usage, after using HTCAPTION, the original double-click maximize event was blocked. The reason is still unknown. In the article "Qt Drag and Resize," a bug was mentioned, and I solved it by using Qt::WindowSystemMenuHint, but for window resizing, I used another approach.
Below is the proxy code to support window dragging. Since the proxy code is lengthy, I only write the key parts. This proxy code was also copied from someone else and modified based on my understanding and project requirements.
Proxy Header File
1 #ifndef NC_FRAMELESS_HELPER_H
2
3 #define NC_FRAMELESS_HELPER_H
4
5 #include <QObject>
6
7 #include <QWidget>
8
9 #include <QRubberBand>
10
11 #include "commonControls/include/commoncontrols_global.h"
12
13 class WidgetResizeHandlerImpl;
14
15 class CRubberBand : public QRubberBand
16
17 {
18
19 public:
20
21 CRubberBand(QRubberBand::Shape s, QWidget * parent = nullptr);
22
23 ~CRubberBand();
24
25 protected:
26
27 virtual void paintEvent(QPaintEvent *) Q_DECL_OVERRIDE;
28
29 virtual void resizeEvent(QResizeEvent *) Q_DECL_OVERRIDE;
30
31 void changeEvent(QEvent *) Q_DECL_OVERRIDE;
32
33 void showEvent(QShowEvent *) Q_DECL_OVERRIDE;
34
35 void moveEvent(QMoveEvent *) Q_DECL_OVERRIDE;
36
37 private:
38
39 };
40
41 // Mouse state, can get the relationship between current cursor and target window
42
43 class CursorPosCalculator
44
45 {
46
47 public:
48
49 CursorPosCalculator(){ reset(); }
50
51 void reset();
52
53 void recalculate(const QPoint& globalMousePos, const QRect& frameRect);
54
55 public:
56
57 bool onEdges;
58
59 bool onLeftEdge;
60
61 bool onRightEdge;
62
63 bool onTopEdge;
64
65 bool onBottomEdge;
66
67 bool onTopLeftEdge;
68
69 bool onBottomLeftEdge;
70
71 bool onTopRightEdge;
72
73 bool onBottomRightEdge;
74
75 static int mBorderWidth;
76
77 };
78
79 // The actual processing class
80
81 class WidgetData
82
83 {
84
85 public:
86
87 WidgetData(WidgetResizeHandlerImpl * d, QWidget* topLevelWidget);
88
89 ~WidgetData();
90
91 QWidget * widget();
92
93 void handleWidgetEvent(QEvent * event);// Entry point for handling specific window events
94
95 void updateRubberBandStatus();
96
97 private:
98
99 void updateCursorShape(const QPoint& globalMousePos);
100
101 void resizeWidget(const QPoint& globalMousePos);
102
103 void moveWidget(const QPoint& globalMousePos);
104
105 void handleMousePressEvent(QMouseEvent* event);
106
107 void handleMouseReleaseEvent(QMouseEvent* event);
108
109 void handleMouseMoveEvent(QMouseEvent* event);
110
111 void handleLeaveEvent(QEvent* event);
112
113 void handleHoverMoveEvent(QHoverEvent* event);
114
115 private:
116
117 bool mLeftButtonPressed = false;
118
119 bool mCursorShapeChanged = false;
120
121 Qt::WindowFlags mWindowFlags;
122
123 QPoint mDragPos;// Start position of drag
124
125 QWidget * mWidget = nullptr;// Pointer to the widget being proxied
126
127 CRubberBand * mRubberBand = nullptr;// Rubber band class, supports rubber band operations
128
129 CursorPosCalculator mPressedMousePos;// Mouse information when pressed
130
131 CursorPosCalculator mMoveMousePos;// Mouse information when moving
132
133 WidgetResizeHandlerImpl * d_ptr;
134
135 };
136
137 /// Description: When QWidget has Qt::FramelessWindowHint attribute, this class can complete: drag + window size change
138
139 class COMMONCONTROLS_EXPORT WidgetResizeHandler : public QObject
140
141 {
142
143 public:
144
145 explicit WidgetResizeHandler(QObject* parent = 0);
146
147 ~WidgetResizeHandler();
148
149 public:
150
151 void activateOn(QWidget * topLevelWidget);// Add top level widget event proxy
152
153 void removeFrom(QWidget * topLevelWidget);// Remove top level widget event proxy
154
155 Qt::CursorShape CursorShape(QWidget * widget);
156
157 // Window movement default: true
158
159 void setWidgetMovable(bool movable);
160
161 bool isWidgetMovable();
162
163 // Size change default: true
164
165 void setWidgetResizable(bool resizable);
166
167 bool isWidgetResizable();
168
169 // Rubber band window movement default: false
170
171 void useRubberBandOnMove(bool use);
172
173 bool isUsingRubberBandOnMove();
174
175 // Rubber band size change default: false
176
177 void useRubberBandOnResize(bool use);
178
179 bool isUsingRubberBandOnResisze();
180
181 void setBorderWidth(int newBorderWidth);
182
183 int borderWidth();
184
185 // Local movable
186
187 void useLocalMoveabled(bool use);
188
189 void addLocalWidget(QWidget *);
190
191 protected:
192
193 virtual bool eventFilter(QObject * obj, QEvent * event) Q_DECL_OVERRIDE;// ????
194
195 private:
196
197 WidgetResizeHandlerImpl * d_ptr;
198
199 };
200
201 #endif // NC_FRAMELESS_HELPER_H
View CodeMost of the comments in the header file are basic, so I won't explain them further. Next, I'll explain the principles. In the class that needs to be proxied, declare a WidgetResizeHandler object and use the activateOn method to add the window to be proxied. Note that the proxied window must have the Qt::Window attribute, meaning it must be a top-level window. When proxying a complex window, unexpected issues may arise, such as QLabel receiving rich text and not getting the mouse release event, or QToolButton not being in the layout and not getting the mouse release event, which can cause the proxy to malfunction. Therefore, I modified the proxy and added the useLocalMoveabled interface to allow the proxy to only move local windows, solving the two problems I mentioned earlier. If you look closely, you can see that my proxy also uses the setGeometry method to drag the window, which differs from what I discussed about the rubber band method. Now comes the key part, hahaha, continue reading. In the header file, there's a class CRubberBand, which inherits from QRubberBand. This class simulates a rubber band process, but the interface provided by Qt is limited. Some features are difficult to achieve, such as implementing a complex proxy interface, where we can only draw ourselves. I re-implemented the paintEvent function to draw a gray rectangle, as shown below:
QPainter p(this);
p.setPen(QPen(QColor(102, 102, 102), 4));
QRect rect = this->rect().adjusted(2, 2, -3, -3);
p.drawRect(rect);
If you follow the steps I mentioned, you will find that in addition to a rectangle, there is also a background color fill. This is strange because our paintEvent does not draw the background. Haha, just add this line in the constructor: setAttribute(Qt::WA_NoSystemBackground). The result is shown in Figure 2.
Figure 2: Custom Rubber Band
Next, I add some proxy code.
- CRubberBand constructor
1 CRubberBand::CRubberBand(QRubberBand::Shape s, QWidget * parent) :QRubberBand(QRubberBand::Rectangle, parent)
2
3 {
4
5 setAttribute(Qt::WA_TranslucentBackground);
6
7 #ifndef Q_DEAD_CODE_FROM_QT4_WIN
8
9 setAttribute(Qt::WA_NoSystemBackground);
10
11 #endif //Q_DEAD_CODE_FROM_QT4_WIN
12
13 setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
14
15 }
View Code2. After activating, store and verify the window ``` 1 WidgetData::WidgetData(WidgetResizeHandlerImpl * d, QWidget * topLevelWidget) 2 3 { 4 5 this->d_ptr = d; 6 7 mWidget = topLevelWidget; 8 9 mWidget->setMouseTracking(true); 10 11 mWindowFlags = mWidget->windowFlags(); 12 13 //mWindowFlags |= Qt::CustomizeWindowHint | Qt::FramelessWindowHint; 14 15 mWindowFlags |= Qt::FramelessWindowHint; 16 17 mWidget->setWindowFlags(mWindowFlags); 18 19 //mWidget->setWindowFlags( Qt::Popup | Qt::CustomizeWindowHint|Qt::FramelessWindowHint ); 20 21 //Bug fix, mouse move events does not propagate from child widgets. 22 23 //so need the hover events. 24 25 mWidget->setAttribute(Qt::WA_Hover); 26 27 updateRubberBandStatus(); 28 29 bool visible = mWidget->isVisible();// prevent non-widget from being proxied 30 31 mWidget->setVisible(visible); 32 33 }
View Code3. When the proxied window has a mouse event, the proxy handles it first ```
1 void WidgetData::handleWidgetEvent(QEvent * event)
2
3 {
4
5 switch (event->type())
6
7 {
8
9 case QEvent::MouseButtonPress:
10
11 handleMousePressEvent(static_cast(event));
12
13 break;
14
15 case QEvent::MouseButtonRelease:
16
17 handleMouseReleaseEvent(static_cast(event));
18
19 break;
20
21 case QEvent::MouseMove:
22
23 handleMouseMoveEvent(static_cast(event));
24
25 break;
26
27 case QEvent::Leave:
28
29 handleLeaveEvent(event);
30
31 break;
32
33 case QEvent::HoverMove:
34
35 handleHoverMoveEvent(static_cast(event));
36
37 break;
38
39 }
40
41 }
View Code4. The following five functions are the specific implementations of the above``` 1 void WidgetData::handleMousePressEvent(QMouseEvent * event) 2 3 { 4 5 if (event->button() == Qt::LeftButton) 6 7 { 8 9 mLeftButtonPressed = true; 10 11 QRect frameRect = mWidget->frameGeometry(); 12 13 mPressedMousePos.recalculate(event->globalPos(), frameRect); 14 15 mDragPos = event->globalPos() - frameRect.topLeft(); 16 17 if (mPressedMousePos.onEdges) 18 19 { 20 21 if (d_ptr->mUseRubberBandOnResize) 22 23 { 24 25 mRubberBand->setGeometry(frameRect); 26 27 //mRubberBand->show(); 28 29 } 30 31 } 32 33 else if (d_ptr->mUseRubberBandOnMove) 34 35 { 36 37 mRubberBand->setGeometry(frameRect); 38 39 //mRubberBand->show(); 40 41 } 42 43 if (d_ptr->mLocalOnMove)// After enabling local drag functionality, need to handle drags outside the specified area and filter them out 44 45 { 46 47 bool canMove = false; 48 49 for (int i = 0; i < d_ptr->mLocalWidget.size(); ++i) 50 51 { 52 53 if (d_ptr->mLocalWidget[i]->rect().contains(d_ptr->mLocalWidget[i]->mapFromGlobal(event->globalPos()))) 54 55 { 56 57 canMove = true; 58 59 break; 60 61 } 62 63 } 64 65 if (canMove == false && mPressedMousePos.onEdges == false) 66 67 { 68 69 mLeftButtonPressed = false; 70 71 } 72 73 } 74 75 } 76 77 } 78 79 void WidgetData::handleMouseReleaseEvent(QMouseEvent* event) 80 81 { 82 83 if (event->button() == Qt::LeftButton) 84 85 { 86 87 d_ptr->mCanMoveFlag = false; 88 89 mLeftButtonPressed = false; 90 91 mPressedMousePos.reset(); 92 93 if (mRubberBand && mRubberBand->isVisible()) 94 95 { 96 97 mRubberBand->hide(); 98 99 mWidget->setGeometry(mRubberBand->geometry()); 100 101 } 102 103 } 104 105 } 106 107 void WidgetData::handleMouseMoveEvent(QMouseEvent* event) 108 109 { 110 111 if (mLeftButtonPressed) 112 113 { 114 115 if (d_ptr->mWidgetResizable && mPressedMousePos.onEdges) 116 117 { 118 119 resizeWidget(event->globalPos()); 120 121 } 122 123 else if (d_ptr->mWidgetMovable) 124 125 { 126 127 moveWidget(event->globalPos()); 128 129 } 130 131 } 132 133 else if (d_ptr->mWidgetResizable) 134 135 { 136 137 updateCursorShape(event->globalPos()); 138 139 } 140 141 } 142 143 void WidgetData::handleLeaveEvent(QEvent*) 144 145 { 146 147 if (!mLeftButtonPressed) 148 149 { 150 151 mWidget->unsetCursor(); 152 153 } 154 155 } 156 157 void WidgetData::handleHoverMoveEvent(QHoverEvent* event) 158 159 { 160 161 if (mLeftButtonPressed) 162 163 { 164 165 return; 166 167 } 168 169 if (d_ptr->mWidgetResizable) 170 171 { 172 173 updateCursorShape(mWidget->mapToGlobal(event->pos())); 174 175 } 176 177 }
View Code5. Update cursor status
1 void WidgetData::updateCursorShape(const QPoint & globalMousePos) 2 3 { 4 5 if (mWidget->isFullScreen() || mWidget->isMaximized()) 6 7 { 8 9 if (mCursorShapeChanged) 10 11 { 12 13 mWidget->unsetCursor(); 14 15 } 16 17 return; 18 19 } 20 21 mMoveMousePos.recalculate(globalMousePos, mWidget->frameGeometry()); 22 23 if (mMoveMousePos.onTopLeftEdge || mMoveMousePos.onBottomRightEdge) 24 25 { 26 27 mWidget->setCursor(Qt::SizeFDiagCursor); 28 29 mCursorShapeChanged = true; 30 31 } 32 33 else if (mMoveMousePos.onTopRightEdge || mMoveMousePos.onBottomLeftEdge) 34 35 { 36 37 mWidget->setCursor(Qt::SizeBDiagCursor); 38 39 mCursorShapeChanged = true; 40 41 } 42 43 else if (mMoveMousePos.onLeftEdge || mMoveMousePos.onRightEdge) 44 45 { 46 47 mWidget->setCursor(Qt::SizeHorCursor); 48 49 mCursorShapeChanged = true; 50 51 } 52 53 else if (mMoveMousePos.onTopEdge || mMoveMousePos.onBottomEdge) 54 55 { 56 57 mWidget->setCursor(Qt::SizeVerCursor); 58 59 mCursorShapeChanged = true; 60 61 } 62 63 else 64 65 { 66 67 if (mCursorShapeChanged)// Modify cursor status 68 69 { 70 71 mWidget->unsetCursor(); 72 73 mCursorShapeChanged = falce; 74 75 } 76 77 } 78 79 }
View Code6. Adjust window size and move position
1 void WidgetData::resizeWidget(const QPoint& globalMousePos) 2 3 { 4 5 QRect origRect; 6 7 if (d_ptr->mUseRubberBandOnResize) 8 9 { 10 11 origRect = mRubberBand->frameGeometry(); 12 13 } 14 15 else 16 17 { 18 19 origRect = mWidget->frameGeometry(); 20 21 } 22 23 int left = origRect.left(); 24 25 int top = origRect.top(); 26 27 int right = origRect.right(); 28 29 int bottom = origRect.bottom(); 30 31 origRect.getCoords(&left, &top, &right, &bottom); 32 33 int minWidth = mWidget->minimumWidth(); 34 35 int minHeight = mWidget->minimumHeight(); 36 37 if (mPressedMousePos.onTopLeftEdge) 38 39 { 40 41 left = globalMousePos.x(); 42 43 top = globalMousePos.y(); 44 45 } 46 47 else if (mPressedMousePos.onBottomLeftEdge) 48 49 { 50 51 left = globalMousePos.x(); 52 53 bottom = globalMousePos.y(); 54 55 } 56 57 else if (mPressedMousePos.onTopRightEdge) 58 59 { 60 61 right = globalMousePos.x(); 62 63 top = globalMousePos.y(); 64 65 } 66 67 else if (mPressedMousePos.onBottomRightEdge) 68 69 { 70 71 right = globalMousePos.x(); 72 73 bottom = globalMousePos.y(); 74 75 } 76 77 else if (mPressedMousePos.onLeftEdge) 78 79 { 80 81 left = globalMousePos.x(); 82 83 int max_width = mWidget->maximumWidth(); 84 85 if (right - left > max_width) 86 87 { 88 89 return; 90 91 } 92 93 } 94 95 else if (mPressedMousePos.onRightEdge) 96 97 { 98 99 right = globalMousePos.x(); 100 101 } 102 103 else if (mPressedMousePos.onTopEdge) 104 105 { 106 107 top = globalMousePos.y(); 108 109 } 110 111 else if (mPressedMousePos.onBottomEdge) 112 113 { 114 115 bottom = globalMousePos.y(); 116 117 } 118 119 QRect newRect(QPoint(left, top), QPoint(right, bottom)); 120 121 if (newRect.isValid()) 122 123 { 124 125 if (minWidth > newRect.width()) 126 127 { 128 129 // Determine what caused the width change. 130 131 if (left != origRect.left()) 132 133 newRect.setLeft(origRect.left()); 134 135 else 136 137 newRect.setRight(origRect.right()); 138 139 } 140 141 if (minHeight > newRect.height()) 142 143 { 144 145 // Determine what caused the height change. 146 147 if (top != origRect.top()) 148 149 newRect.setTop(origRect.top()); 150 151 else 152 153 newRect.setBottom(origRect.bottom()); 154 155 } 156 157 if (d_ptr->mUseRubberBandOnResize) 158 159 { 160 161 if (mRubberBand->isVisible() == false) 162 163 { 164 165 mRubberBand->show(); 166 167 } 168 169 mRubberBand->setGeometry(newRect); 170 171 } 172 173 else 174 175 { 176 177 //mWidget->setGeometry(newRect); 178 179 mWidget->move(newRect.topLeft()); 180 181 mWidget->resize(newRect.size()); 182 183 } 184 185 } 186 187 else 188 189 { 190 191 //qDebug() << "Calculated Rect is not valid" << newRect; 192 193 } 194 195 } 196 197 void WidgetData::moveWidget(const QPoint & globalMousePos) 198 199 { 200 201 bool canMove = false; 202 203 if (d_ptr->mLocalOnMove == true && d_ptr->mCanMoveFlag != true) 204 205 { 206 207 for (int i = 0; i < d_ptr->mLocalWidget.size(); ++i) 208 209 { 210 211 if (d_ptr->mLocalWidget[i]->rect().contains(d_ptr->mLocalWidget[i]->mapFromGlobal(globalMousePos))) 212 213 { 214 215 canMove = true; 216 217 d_ptr->mCanMoveFlag = true; 218 219 break; 220 221 } 222 223 } 224 225 } 226 227 else 228 229 { 230 231 canMove = true; 232 233 } 234 235 if (canMove) 236 237 { 238 239 if (d_ptr->mUseRubberBandOnMove) 240 241 { 242 243 if (mRubberBand->isVisible() == false) 244 245 { 246 247 mRubberBand->show(); 248 249 } 250 251 mRubberBand->move(globalMousePos - mDragPos); 252 253 } 254 255 else 256 257 { 258 259 mWidget->move(globalMousePos - mDragPos); 260 261 } 262 263 } 264 265 }
View Code