前言
Qt自带的QDockWidget可实现停靠在主窗口中的小部件,可以独立地停靠在主窗口的上、下、左、右四个边缘位置,也可以相互拖拽合并成一个tab组,但是想要将dock窗体停留在中间区域,模拟VS那种中间为DocumentHost的布局就不太可行,尽管可以使用splitDockWidget()方法将dock窗体停留在中间区域,但是效果还是不太理想。
参考了其他平台关于dock布局,最终决定使用QTabWidget+QDockWidget来实现,效果如图所示。
一、自定义KwDockLayout组件
其中根据DockPanelParam类中的DockWidgetArea属性,自动转化创建对应的dock窗体或tab页签
,其中levelCode表示窗体的层级父子关系,在关闭窗体时自动关闭其子窗体。
class DockPanelParam : public QObject {
Q_OBJECT
public:
Qt::DockWidgetArea area;
KwWidget* widget;
QString title;
QString toolTip;
QString levelCode;
};
Q_DECLARE_METATYPE(DockPanelParam*)
KwDockLayout里设置了QTab与QDock的样式,及窗体的销毁
class KwDockLayout : public QTabWidget {
Q_OBJECT
public:
explicit KwDockLayout(QMainWindow* parent);
friend class DockManager;
public slots:
void closeTab(int index);
void closeOtherTabs(int index);
void closeAllTabs();
void nextTab();
void previousTab();
void createLayoutPanel(DockPanelParam* panel);
void closeChildDockWidget(QString levelCode);
private slots:
void handleCurrentChanged(int index);
void handleContextMenuRequested(const QPoint& pos);
void destroyDockWidget(QString name);
private:
QMainWindow* mainWindow;
QHash<QString, DockPanelModel*> panels;
};
#include "kwdocklayout.h"
#include <QMenu>
#include <QTabBar>
KwDockLayout::KwDockLayout(QMainWindow* parent)
: QTabWidget(parent)
{
mainWindow = parent;
QTabBar* tabBar = this->tabBar();
tabBar->setTabsClosable(true);
tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab);
tabBar->setMovable(true);
tabBar->setContextMenuPolicy(Qt::CustomContextMenu);
connect(tabBar, &QTabBar::customContextMenuRequested, this, &KwDockLayout::handleContextMenuRequested);
connect(tabBar, &QTabBar::tabCloseRequested, this, &KwDockLayout::closeTab);
setElideMode(Qt::ElideRight);
QStringList qss;
qss.append("QTabWidget{background: #ffffff;}");
qss.append("QTabWidget::pane {border: 1px solid #f0f0f0; border-top: 3px solid #007aec;padding-top:2px; }");
qss.append("QTabBar::tab{background: #f0f0f0; color: #000000; font-size: 12px; text-indent: 2; text-align: left top; min-width: 50px; min-height:25px; max-width: 200px;}");
qss.append("QTabBar::tab:hover:!selected {background-color: #007aec; color: #ffffff;}");
qss.append("QTabBar::tab:selected {background-color: #007aec; color: #ffffff;}");
qss.append("QTabBar::tab:!selected {background-color: #ffffff; color: #000000;}");
this->setStyleSheet(qss.join(""));
DockManager::Ins->setDockLayout(this);
connect(this, &QTabWidget::currentChanged, this, &KwDockLayout::handleCurrentChanged);
}
void KwDockLayout::handleCurrentChanged(int index)
{
if (index != -1) {
QWidget* view = widget(index);
view->setFocus();
}
}
void KwDockLayout::handleContextMenuRequested(const QPoint& pos)
{
QMenu menu;
int index = tabBar()->tabAt(pos);
if (index != -1) {
QAction* action = menu.addAction(tr("关闭"));
action->setShortcut(QKeySequence::Close);
connect(action, &QAction::triggered, this, [this, index]() {
closeTab(index);
});
action = menu.addAction(tr("除此之外全部关闭"));
connect(action, &QAction::triggered, this, [this, index]() {
closeOtherTabs(index);
});
} else {
menu.addSeparator();
}
menu.exec(QCursor::pos());
}
void KwDockLayout::createLayoutPanel(DockPanelParam* param)
{
DockPanelModel* panel = nullptr;
for (QString key : panels.keys()) {
if (key == param->widget->Key) {
if (panels[key]) {
destroyDockWidget(key);
}
}
}
panel = new DockPanelModel();
if (param->area == Qt::NoDockWidgetArea) {
param->widget->setParent(this);
int index = addTab(param->widget, param->title);
setCurrentIndex(index);
panel->isDock = false;
panel->widget = param->widget;
setTabToolTip(index, param->toolTip);
setTabShape(QTabWidget::Rounded);
//setTabIcon(index, webView->favIcon());
panel->widget->layout()->setMargin(0);
} else {
QDockWidget* dw = new QDockWidget(param->title, this);
QStringList qss;
qss.append("QDockWidget{font-size: 20px; font-weight: 500; color: #007aec; border: 1px solid gray; padding:5px}");
qss.append("QDockWidget::title{ background: #ffffff; padding-left: 5px;}");
dw->setStyleSheet(qss.join(""));
dw->setObjectName(param->widget->Key);
dw->setToolTip(param->toolTip);
dw->setWidget(param->widget);
param->widget->layout()->setMargin(2);
param->widget->setParent(dw);
panel->isDock = true;
panel->dock = dw;
mainWindow->addDockWidget(param->area, dw);
}
panel->widget = param->widget;
panel->key = param->widget->Key;
panel->levelCode = param->levelCode;
panels.insert(panel->key, panel);
delete param;
}
void KwDockLayout::closeOtherTabs(int index)
{
for (int i = count() - 1; i > index; --i)
closeTab(i);
for (int i = index - 1; i >= 0; --i)
closeTab(i);
}
void KwDockLayout::closeAllTabs()
{
for (int i = count() - 1; i >= 0; --i)
closeTab(i);
}
void KwDockLayout::closeTab(int index)
{
if (QWidget* view = widget(index)) {
KwWidget* wd = qobject_cast<KwWidget*>(view);
if (wd) {
destroyDockWidget(wd->Key);
}
}
}
void KwDockLayout::nextTab()
{
int next = currentIndex() + 1;
if (next == count())
next = 0;
setCurrentIndex(next);
}
void KwDockLayout::previousTab()
{
int next = currentIndex() - 1;
if (next < 0)
next = count() - 1;
setCurrentIndex(next);
}
void KwDockLayout::destroyDockWidget(QString name)
{
if (panels[name]) {
closeChildDockWidget(panels[name]->levelCode);
DockPanelModel* p = panels[name];
if (p) {
if (p->isDock) {
if (p->dock) {
mainWindow->removeDockWidget(p->dock);
if (p->dock) {
p->dock->close();
delete p->dock;
p->dock = NULL;
}
} else {
if (p->widget) {
delete p->widget;
p->widget = NULL;
}
}
} else {
removeTab(indexOf(p->widget));
if (p->widget) {
delete p->widget;
p->widget = NULL;
}
}
delete p;
}
}
panels.remove(name);
}
void KwDockLayout::closeChildDockWidget(QString levelCode)
{
for (QString key : panels.keys()) {
if (panels[key]) {
if (panels[key]->levelCode.startsWith(levelCode) && panels[key]->levelCode != levelCode) {
if (panels[key]) {
destroyDockWidget(key);
}
}
}
}
}
二、自定义DockManager
用于全局管理窗体,获取窗体对象、校验、激活等功能
class DockPanelModel : public QObject {
Q_OBJECT
public:
bool isDock;
KwWidget* widget;
QDockWidget* dock;
QString key;
QString levelCode;
};
class KwDockLayout;
class DockManager : public QObject {
public:
DockManager();
static DockManager* Ins;
friend class KwDockLayout;
DockPanelModel* getPanel(QString key);
bool containsKey(QString key);
void closeChild(QString levelCode);
QString defaultLevelCode(QString levelCode);
void active(DockPanelModel* panel);
private:
void setDockLayout(KwDockLayout* dock)
{
_dock = dock;
}
KwDockLayout* _dock;
};
#include "dockmanager.h"
#include "kwdocklayout.h"
DockManager* DockManager::Ins = new DockManager();
DockManager::DockManager()
{
}
DockPanelModel* DockManager::getPanel(QString key)
{
if (_dock) {
if (_dock->panels.contains(key)) {
return _dock->panels[key];
}
}
return 0;
}
void DockManager::active(DockPanelModel* panel)
{
if (!panel) {
return;
}
if (panel->isDock) {
panel->dock->activateWindow();
} else {
_dock->setCurrentWidget(panel->widget);
}
}
bool DockManager::containsKey(QString key)
{
if (_dock) {
if (_dock->panels.contains(key)) {
return true;
}
}
return false;
}
void DockManager::closeChild(QString levelCode)
{
if (_dock) {
_dock->closeChildDockWidget(levelCode);
}
}
QString DockManager::defaultLevelCode(QString levelCode)
{
return QString("%1-%2").arg(levelCode).arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
}
三、使用方式
构建UiEx扩展类用于创建窗体,通过信号槽传递至KwDockLayout中
class KWWIDGETBASE_EXPORT UiEx : public QObject {
Q_OBJECT
public:
UiEx();
static UiEx* MyUiEx;
static void ShowDocumentDockWidget(KwWidget* wd, QString title, QString toolTip, Qt::DockWidgetArea area = Qt::NoDockWidgetArea){
DockPanelParam* param = new DockPanelParam();
param->area = area;
param->levelCode = wd->LevelCode;
param->widget = wd;
param->title = title;
param->toolTip = toolTip;
emit MyUiEx->onShowDocumentDockWidget(param);
}
signals:
void onShowDocumentDockWidget(DockPanelParam* dw);
};
在mainwindow中添加KwDockLayout,并建立信号关系
DockOptions opts;
opts |= AllowNestedDocks;
opts |= AllowTabbedDocks;
//AllowNestedDocks//允许内嵌
//AllowTabbedDocks//允许选项卡式
//ForceTabbedDocks//强迫选项卡式
QMainWindow::setDockOptions(opts);
this->setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); //设置dock窗体左上区域由左窗体优先使用
this->setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea);
this->setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
this->setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
KwDockLayout*mDockLayout = new KwDockLayout(this);
this->setCentralWidget(mDockLayout);
connect(UiEx::MyUiEx, &UiEx::onShowDocumentDockWidget, mDockLayout, &KwDockLayout::createLayoutPanel);
在任意界面即可使用UiEx创建窗体
KwWidget* wd= new KwWidget(nullptr);
wd->Key = "tree1";
wd->LevelCode = "tree1";
wd->setLayout(new QVBoxLayout());
UiEx::ShowDocumentDockWidget(wd, "dock1", "dock1", Qt::LeftDockWidgetArea);
KwWidget* wd2= new KwWidget(nullptr);
wd2->Key = "tree2";
wd2->LevelCode = "tree2";
wd2->setLayout(new QVBoxLayout());
UiEx::ShowDocumentDockWidget(wd2, "dock2", "dock2", Qt::LeftDockWidgetArea);
KwWidget* wd3= new KwWidget(nullptr);
wd3->Key = "docm1";
wd3->LevelCode = "tree1-docm1";
wd3->setLayout(new QVBoxLayout());
UiEx::ShowDocumentDockWidget(wd3, "docm1", "docm1", Qt::NoDockWidgetArea);
KwWidget* wd4= new KwWidget(nullptr);
wd4->Key = "docm2";
wd4->LevelCode = "tree1-docm2";
wd4->setLayout(new QVBoxLayout());
UiEx::ShowDocumentDockWidget(wd4, "docm2", "docm2", Qt::NoDockWidgetArea);
KwWidget* wd5= new KwWidget(nullptr);
wd5->Key = "prt";
wd5->LevelCode = "tree1-doc2-prt1";
wd5->setLayout(new QVBoxLayout());
UiEx::ShowDocumentDockWidget(wd5, "Right", "Right", Qt::RightDockWidgetArea);
KwWidget* wd6= new KwWidget(nullptr);
wd6->Key = "prt2";
wd6->LevelCode = "tree1-doc2-prt2";
wd6->setLayout(new QVBoxLayout());
UiEx::ShowDocumentDockWidget(wd6, "Bottom", "Bottom", Qt::BottomDockWidgetArea);
通过DockManager判断窗体是否存在,关闭层级窗体
if (DockManager::Ins->containsKey(key)) {
return;
}
DockManager::Ins->closeChild(this->LevelCode);
总结
KwDockLayout提供了一个方便易用的机制来管理QDockWidget部件的布局,使得用户可以灵活地调整和重新排列部件的位置和大小。使用KwDockLayout可以实现复杂的实时界面布局。