基于Qt实现跨平台应用的Photoshop式工具箱拖拽停靠效果
Hey there, let's build that custom dockable toolbox for your cross-platform Qt app—exactly the Photoshop-style corner docking you want, no QToolbar required. This approach works smoothly on Windows, Mac, and Linux, and gives you full control over the behavior.
Core Idea
We'll create a custom QWidget for the toolbox, handle mouse drag events manually, detect when the toolbox gets close to your main window's corners, and snap it into place when the user releases the mouse. No relying on Qt's built-in dock widgets—we're doing this from scratch for full customization.
Step-by-Step Implementation
1. Build the Custom Toolbox Widget
First, create a ToolBoxWidget class that inherits from QWidget. We'll override mouse events to handle dragging, add corner detection logic, and implement the snap-to-corner behavior.
#include <QWidget> #include <QMouseEvent> #include <QLineF> class ToolBoxWidget : public QWidget { Q_OBJECT public: explicit ToolBoxWidget(QWidget *parent = nullptr) : QWidget(parent) { // Set initial size and styling to match a typical toolbox setFixedSize(200, 300); setStyleSheet("background-color: #f0f0f0; border: 1px solid #cccccc; border-radius: 4px;"); // Frameless window so we can drag it freely, keep it above other windows setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); } // Pass a pointer to the main window so we can track its corners void setMainWindow(QWidget* mainWin) { m_mainWindow = mainWin; } protected: void mousePressEvent(QMouseEvent *event) override { if (event->button() == Qt::LeftButton) { // Record the offset between the mouse click and the widget's top-left corner m_dragOffset = event->globalPos() - frameGeometry().topLeft(); event->accept(); } } void mouseMoveEvent(QMouseEvent *event) override { if (event->buttons() & Qt::LeftButton) { // Move the widget with the mouse move(event->globalPos() - m_dragOffset); // Check if we're near a main window corner for visual feedback updateCornerProximity(); event->accept(); } } void mouseReleaseEvent(QMouseEvent *event) override { if (event->button() == Qt::LeftButton) { // Snap to the closest corner when the user lets go snapToClosestCorner(); // Reset styling back to normal setStyleSheet("background-color: #f0f0f0; border: 1px solid #cccccc; border-radius: 4px;"); event->accept(); } } private: QPoint m_dragOffset; QWidget* m_mainWindow = nullptr; const int m_cornerSnapRange = 150; // 150px range around each corner to trigger snapping void updateCornerProximity() { if (!m_mainWindow) return; QRect mainWindowBounds = m_mainWindow->frameGeometry(); QPoint toolboxCenter = frameGeometry().center(); // Check if we're within snap range of any corner bool nearCorner = false; nearCorner |= QLineF(toolboxCenter, mainWindowBounds.topLeft()).length() < m_cornerSnapRange; nearCorner |= QLineF(toolboxCenter, mainWindowBounds.topRight()).length() < m_cornerSnapRange; nearCorner |= QLineF(toolboxCenter, mainWindowBounds.bottomLeft()).length() < m_cornerSnapRange; nearCorner |= QLineF(toolboxCenter, mainWindowBounds.bottomRight()).length() < m_cornerSnapRange; // Give visual feedback: highlight the border when near a corner if (nearCorner) { setStyleSheet("background-color: #f0f0f0; border: 2px solid #0078d7; border-radius: 4px;"); } else { setStyleSheet("background-color: #f0f0f0; border: 1px solid #cccccc; border-radius: 4px;"); } } void snapToClosestCorner() { if (!m_mainWindow) return; QRect mainWindowBounds = m_mainWindow->frameGeometry(); QPoint toolboxCenter = frameGeometry().center(); // Calculate distance to each main window corner int distTopLeft = QLineF(toolboxCenter, mainWindowBounds.topLeft()).length(); int distTopRight = QLineF(toolboxCenter, mainWindowBounds.topRight()).length(); int distBottomLeft = QLineF(toolboxCenter, mainWindowBounds.bottomLeft()).length(); int distBottomRight = QLineF(toolboxCenter, mainWindowBounds.bottomRight()).length(); // Find the closest corner int minDist = qMin(qMin(distTopLeft, distTopRight), qMin(distBottomLeft, distBottomRight)); QPoint targetPos; // Offset the toolbox slightly from the corner so it doesn't stick directly to the edge const int edgeOffset = 10; if (minDist == distTopLeft) { targetPos = mainWindowBounds.topLeft() + QPoint(edgeOffset, edgeOffset); } else if (minDist == distTopRight) { targetPos = mainWindowBounds.topRight() - QPoint(width() + edgeOffset, -edgeOffset); } else if (minDist == distBottomLeft) { targetPos = mainWindowBounds.bottomLeft() + QPoint(edgeOffset, -height() - edgeOffset); } else { targetPos = mainWindowBounds.bottomRight() - QPoint(width() + edgeOffset, height() + edgeOffset); } // Move the toolbox to the target corner move(targetPos); } };
2. Integrate with Your Main Window
In your main window, initialize the toolbox and pass a reference to the main window so the toolbox can track its position:
#include "ToolBoxWidget.h" #include <QMainWindow> class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { // Set main window size and position setGeometry(100, 100, 800, 600); setWindowTitle("Custom Dockable Toolbox Demo"); // Create and configure the toolbox m_toolBox = new ToolBoxWidget(this); m_toolBox->setMainWindow(this); // Set initial position for the toolbox m_toolBox->move(200, 200); m_toolBox->show(); } private: ToolBoxWidget* m_toolBox = nullptr; };
3. Cross-Platform Tweaks
- Windows: The
Qt::FramelessWindowHintworks well here, but if you notice any odd behavior with window dragging, you can add a smallQWidgetas a title bar area (though our current implementation doesn't need it). - Mac: The
Qt::WindowStaysOnTopHintworks as expected, but if you need the toolbox to stay above full-screen apps, you might need to tweak the window level using native macOS APIs (but for most cases, the default is fine). - Linux: Test on GNOME and KDE to ensure the frameless window drag works—some desktop environments require extra hints, but Qt handles most of this out of the box.
4. Enhancements (Optional)
- Multiple Toolboxes: Add support for multiple toolboxes by creating instances of
ToolBoxWidgetand ensuring each can snap independently. - Edge Docking: Extend the logic to support snapping to the main window's left/right/top/bottom edges, not just corners.
- Persistent Position: Save the toolbox's position to
QSettingsso it remembers where it was docked when the app restarts.
内容的提问来源于stack exchange,提问作者Moussa Ball

