在QtCreator中用 QListWidget + QStackedWidget 模仿vs code TabWidget
在QtCreator中用 QListWidget + QStackedWidget 模仿vs code TabWidget
一、自定义委托和标签栏
vscodeTabBar.h
#// vscodeTabBar.h - 自定义标签栏(基于 QListWidget)
#ifndef VSCODETABBAR_H
#define VSCODETABBAR_H
#include <QListWidget>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QContextMenuEvent>
#include <QMenu>
#include <QAction>
#include <QStyledItemDelegate>
#include <QPainter>
// 自定义标签项数据角色
enum TabDataRole
{
TabFilePathRole = Qt::UserRole + 1,
TabIsModifiedRole,
TabIsPinnedRole
};
// 自定义绘制委托,实现 VS Code 风格的标签外观
class VSCodeTabDelegate : public QStyledItemDelegate
{
public:
explicit VSCodeTabDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
painter->save();
QRect rect = option.rect;
bool isSelected = option.state & QStyle::State_Selected;
bool isHovered = option.state & QStyle::State_MouseOver;
bool isModified = index.data(TabIsModifiedRole).toBool();
bool isPinned = index.data(TabIsPinnedRole).toBool();
QString text = index.data(Qt::DisplayRole).toString();
// 背景色
if (isSelected) {
painter->fillRect(rect, QColor("#1e1e1e"));
// 顶部蓝色指示条
painter->fillRect(QRect(rect.left(), rect.top(), rect.width(), 2), QColor("#007acc"));
} else if (isHovered) {
painter->fillRect(rect, QColor("#2a2d2e"));
} else {
painter->fillRect(rect, QColor("#2d2d30"));
}
// 分隔线
painter->setPen(QColor("#252526"));
painter->drawLine(rect.right(), rect.top() + 4, rect.right(), rect.bottom() - 4);
// 图标区域(左侧)
int leftMargin = 12;
int iconSize = 16;
// 绘制文件图标(简化为彩色圆点)
QColor fileColor = getFileColor(text);
painter->setBrush(fileColor);
painter->setPen(Qt::NoPen);
painter->drawEllipse(QPoint(rect.left() + leftMargin + iconSize/2,
rect.top() + rect.height()/2),
iconSize/2 - 2, iconSize/2 - 2);
// 固定标签图标
if (isPinned) {
painter->setPen(QColor("#cca700"));
painter->setBrush(Qt::NoBrush);
painter->drawText(QRect(rect.left() + leftMargin, rect.top(), iconSize, rect.height()),
Qt::AlignCenter, "📌");
}
// 文本
QRect textRect = rect.adjusted(leftMargin + iconSize + 8, 0, -28, 0);
QFont font = painter->font();
font.setPointSize(11);
if (isModified) {
font.setItalic(true);
}
painter->setFont(font);
QPen textPen;
if (isSelected) {
textPen.setColor(QColor("#ffffff"));
} else if (isModified) {
textPen.setColor(QColor("#cccccc"));
} else {
textPen.setColor(QColor("#969696"));
}
painter->setPen(textPen);
// 省略号处理
QFontMetrics fm(font);
QString elidedText = fm.elidedText(text, Qt::ElideRight, textRect.width());
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, elidedText);
// 修改指示点(白色圆点)
if (isModified && !isSelected) {
painter->setBrush(QColor("#ffffff"));
painter->setPen(Qt::NoPen);
painter->drawEllipse(QPoint(rect.right() - 20, rect.top() + rect.height()/2), 3, 3);
}
// 关闭按钮区域
QRect closeRect(rect.right() - 24, rect.top() + (rect.height()-16)/2, 16, 16);
if (isHovered || isSelected) {
painter->setPen(QColor("#cccccc"));
painter->setBrush(Qt::NoBrush);
// 绘制 X
painter->drawLine(closeRect.left() + 4, closeRect.top() + 4,
closeRect.right() - 4, closeRect.bottom() - 4);
painter->drawLine(closeRect.right() - 4, closeRect.top() + 4,
closeRect.left() + 4, closeRect.bottom() - 4);
}
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
Q_UNUSED(option)
Q_UNUSED(index)
return QSize(140, 35);
}
private:
QColor getFileColor(const QString &filename) const
{
if (filename.endsWith(".cpp") || filename.endsWith(".c") || filename.endsWith(".h"))
return QColor("#519aba");
else if (filename.endsWith(".py"))
return QColor("#3572A5");
else if (filename.endsWith(".js") || filename.endsWith(".ts"))
return QColor("#f1e05a");
else if (filename.endsWith(".html") || filename.endsWith(".xml"))
return QColor("#e34c26");
else if (filename.endsWith(".css"))
return QColor("#563d7c");
else if (filename.endsWith(".json"))
return QColor("#292929");
else if (filename.endsWith(".md"))
return QColor("#083fa1");
else if (filename.endsWith(".txt"))
return QColor("#89e051");
else
return QColor("#a8a8a8");
}
};
// VS Code 风格标签栏
class VSCodeTabBar : public QListWidget
{
Q_OBJECT
public:
explicit VSCodeTabBar(QWidget *parent = nullptr) : QListWidget(parent)
{
setFlow(QListWidget::LeftToRight);
setWrapping(false);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setSelectionMode(QAbstractItemView::SingleSelection);
setSpacing(0);
setContentsMargins(0, 0, 0, 0);
setFrameShape(QFrame::NoFrame);
setIconSize(QSize(16, 16));
setUniformItemSizes(false);
// 设置自定义委托
setItemDelegate(new VSCodeTabDelegate(this));
// 样式
setStyleSheet(R"(
QListWidget {
background-color: #2d2d30;
border: none;
outline: none;
padding: 0px;
}
QListWidget::item {
border: none;
padding: 0px;
margin: 0px;
}
)");
setFixedHeight(35);
connect(this, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) {
emit tabClicked(row(item));
});
}
// 添加标签页
int addTab(const QString &text, const QString &filePath = QString())
{
QListWidgetItem *item = new QListWidgetItem(text);
item->setData(TabFilePathRole, filePath);
item->setData(TabIsModifiedRole, false);
item->setData(TabIsPinnedRole, false);
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
addItem(item);
return count() - 1;
}
// 设置标签修改状态
void setTabModified(int index, bool modified)
{
if (index >= 0 && index < count()) {
item(index)->setData(TabIsModifiedRole, modified);
updateItem(item(index));
}
}
// 设置标签固定状态
void setTabPinned(int index, bool pinned)
{
if (index >= 0 && index < count()) {
item(index)->setData(TabIsPinnedRole, pinned);
updateItem(item(index));
}
}
// 获取标签文本
QString tabText(int index) const
{
if (index >= 0 && index < count())
return item(index)->text();
return QString();
}
// 设置标签文本
void setTabText(int index, const QString &text)
{
if (index >= 0 && index < count()) {
item(index)->setText(text);
updateItem(item(index));
}
}
// 关闭标签
void closeTab(int index)
{
if (index >= 0 && index < count()) {
QListWidgetItem *itemToRemove = takeItem(index);
delete itemToRemove;
emit tabClosed(index);
}
}
// 鼠标滚轮切换标签
void wheelEvent(QWheelEvent *event) override
{
int delta = event->angleDelta().y();
int current = currentRow();
if (delta > 0 && current > 0) {
setCurrentRow(current - 1);
emit tabClicked(current - 1);
} else if (delta < 0 && current < count() - 1) {
setCurrentRow(current + 1);
emit tabClicked(current + 1);
}
event->accept();
}
// 右键菜单
void contextMenuEvent(QContextMenuEvent *event) override
{
QListWidgetItem *item = itemAt(event->pos());
if (!item) return;
int index = row(item);
bool isPinned = item->data(TabIsPinnedRole).toBool();
QMenu menu(this);
menu.setStyleSheet(R"(
QMenu {
background-color: #3c3c3c;
color: #cccccc;
border: 1px solid #454545;
padding: 4px;
}
QMenu::item {
padding: 6px 24px;
border-radius: 3px;
}
QMenu::item:selected {
background-color: #094771;
}
QMenu::separator {
height: 1px;
background-color: #454545;
margin: 4px 8px;
}
)");
QAction *closeAct = menu.addAction("关闭");
QAction *closeOthersAct = menu.addAction("关闭其他");
QAction *closeRightAct = menu.addAction("关闭右侧");
menu.addSeparator();
QAction *pinAct = menu.addAction(isPinned ? "取消固定" : "固定");
menu.addSeparator();
QAction *copyPathAct = menu.addAction("复制路径");
QAction *selected = menu.exec(event->globalPos());
if (selected == closeAct) {
emit tabCloseRequested(index);
} else if (selected == closeOthersAct) {
emit closeOtherTabsRequested(index);
} else if (selected == closeRightAct) {
emit closeRightTabsRequested(index);
} else if (selected == pinAct) {
setTabPinned(index, !isPinned);
emit tabPinned(index, !isPinned);
} else if (selected == copyPathAct) {
QString path = item->data(TabFilePathRole).toString();
if (!path.isEmpty()) {
emit copyPathRequested(path);
}
}
}
// 点击关闭按钮检测
void mousePressEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton) {
QListWidgetItem *clickedItem = itemAt(event->pos());
if (clickedItem) {
int index = row(clickedItem);
QRect itemRect = visualItemRect(clickedItem);
// 关闭按钮区域
QRect closeRect(itemRect.right() - 24, itemRect.top() + (itemRect.height()-16)/2, 20, itemRect.height());
if (closeRect.contains(event->pos())) {
emit tabCloseRequested(index);
return;
}
}
}
QListWidget::mousePressEvent(event);
}
signals:
void tabClicked(int index);
void tabCloseRequested(int index);
void tabClosed(int index);
void closeOtherTabsRequested(int index);
void closeRightTabsRequested(int index);
void tabPinned(int index, bool pinned);
void copyPathRequested(const QString &path);
private:
void updateItem(QListWidgetItem *item)
{
QModelIndex idx = indexFromItem(item);
emit dataChanged(idx, idx);
}
};
#endif // VSCODETABBAR_H
二、自定义编辑页和标签页
#ifndef VSCODETABWIDGET_H
#define VSCODETABWIDGET_H
#include "vscodeTabBar.h"
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QStackedWidget>
#include <QPushButton>
#include <QLabel>
#include <QTextEdit>
// 自定义编辑页
class EditorPage : public QWidget
{
Q_OBJECT
public:
explicit EditorPage(const QString &filePath, QWidget *parent = nullptr)
: QWidget(parent), m_filePath(filePath), m_modified(false)
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
// 面包屑导航栏
QWidget *breadcrumb = new QWidget;
breadcrumb->setFixedHeight(28);
breadcrumb->setStyleSheet("background-color: #1e1e1e; border-bottom: 1px solid #333333;");
QHBoxLayout *breadLayout = new QHBoxLayout(breadcrumb);
breadLayout->setContentsMargins(12, 0, 12, 0);
breadLayout->setSpacing(4);
QStringList parts = filePath.split('/');
for (int i = 0; i < parts.size(); ++i) {
if (parts[i].isEmpty()) continue;
QLabel *label = new QLabel(parts[i]);
label->setStyleSheet("color: #969696; font-size: 11px;");
breadLayout->addWidget(label);
if (i < parts.size() - 1) {
QLabel *sep = new QLabel(">");
sep->setStyleSheet("color: #666666; font-size: 11px; padding: 0 4px;");
breadLayout->addWidget(sep);
}
}
breadLayout->addStretch();
layout->addWidget(breadcrumb);
// 编辑器区域
m_editor = new QTextEdit; // ← 这里创建对象
m_editor->setStyleSheet(R"(
QTextEdit {
background-color: #1e1e1e;
color: #d4d4d4;
border: none;
padding: 12px;
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 14px;
line-height: 22px;
selection-background-color: #264f78;
}
QScrollBar:vertical {
background-color: #1e1e1e;
width: 14px;
border-radius: 0px;
}
QScrollBar::handle:vertical {
background-color: #424242;
border-radius: 7px;
min-height: 30px;
margin: 2px;
}
QScrollBar::handle:vertical:hover {
background-color: #4f4f4f;
}
)");
if (filePath.endsWith(".cpp") || filePath.endsWith(".h")) {
m_editor->setPlainText(generateCppCode());
} else if (filePath.endsWith(".py")) {
m_editor->setPlainText(generatePythonCode());
} else {
m_editor->setPlainText("// " + filePath + "\n// 在此编辑文件内容...\n");
}
connect(m_editor, &QTextEdit::textChanged, this, [this]() {
if (!m_modified) {
m_modified = true;
emit contentModified(m_modified);
}
});
layout->addWidget(m_editor);
}
QString filePath() const { return m_filePath; }
bool isModified() const { return m_modified; }
void setModified(bool modified) { m_modified = modified; }
QString content() const { return m_editor->toPlainText(); }
void setContent(const QString &text) { m_editor->setPlainText(text); }
signals:
void contentModified(bool modified);
private:
QString m_filePath;
bool m_modified;
QTextEdit *m_editor;
QString generateCppCode()
{
return R"(#include <iostream>
#include <vector>
#include <string>
class TabWidget : public QWidget {
public:
TabWidget(QWidget* parent = nullptr);
void addTab(const QString& title, QWidget* widget);
void closeTab(int index);
private:
std::vector<QWidget*> m_tabs;
};
int main() {
std::cout << "Hello, VS Code Style!" << std::endl;
return 0;
})";
}
QString generatePythonCode()
{
return R"(#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from PyQt6.QtWidgets import *
class VSCodeTabWidget(QTabWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setStyleSheet("""
QTabBar::tab {
background: #2d2d30;
color: #969696;
}
""")
def add_editor(self, file_path: str) -> None:
editor = QTextEdit()
self.addTab(editor, file_path)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = VSCodeTabWidget()
window.show()
sys.exit(app.exec())
)";
}
};
// ==================== VSCodeTabWidget ====================
// 自动义标签页
class VSCodeTabWidget : public QWidget
{
Q_OBJECT
public:
explicit VSCodeTabWidget(QWidget *parent = nullptr) : QWidget(parent)
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
QWidget *tabBarContainer = new QWidget;
tabBarContainer->setFixedHeight(35);
tabBarContainer->setStyleSheet("background-color: #2d2d30;");
QHBoxLayout *tabBarLayout = new QHBoxLayout(tabBarContainer);
tabBarLayout->setContentsMargins(0, 0, 0, 0);
tabBarLayout->setSpacing(0);
m_tabBar = new VSCodeTabBar;
connect(m_tabBar, &VSCodeTabBar::tabClicked, this, &VSCodeTabWidget::setCurrentIndex);
connect(m_tabBar, &VSCodeTabBar::tabCloseRequested, this, &VSCodeTabWidget::closeTab);
connect(m_tabBar, &VSCodeTabBar::closeOtherTabsRequested, this, &VSCodeTabWidget::closeOtherTabs);
connect(m_tabBar, &VSCodeTabBar::closeRightTabsRequested, this, &VSCodeTabWidget::closeRightTabs);
tabBarLayout->addWidget(m_tabBar);
QPushButton *newTabBtn = new QPushButton("+");
newTabBtn->setFixedSize(35, 35);
newTabBtn->setStyleSheet(R"(
QPushButton {
background-color: #2d2d30;
color: #cccccc;
border: none;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3c3c3c;
}
)");
connect(newTabBtn, &QPushButton::clicked, this, [this]() {
emit newTabRequested();
});
tabBarLayout->addWidget(newTabBtn);
QPushButton *moreBtn = new QPushButton("⋮");
moreBtn->setFixedSize(35, 35);
moreBtn->setStyleSheet(newTabBtn->styleSheet());
tabBarLayout->addWidget(moreBtn);
mainLayout->addWidget(tabBarContainer);
QWidget *sep = new QWidget;
sep->setFixedHeight(1);
sep->setStyleSheet("background-color: #252526;");
mainLayout->addWidget(sep);
m_stack = new QStackedWidget;
m_stack->setStyleSheet("background-color: #1e1e1e;");
mainLayout->addWidget(m_stack);
QWidget *statusBar = new QWidget;
statusBar->setFixedHeight(22);
statusBar->setStyleSheet(R"(
background-color: #007acc;
color: #ffffff;
font-size: 11px;
)");
QHBoxLayout *statusLayout = new QHBoxLayout(statusBar);
statusLayout->setContentsMargins(12, 0, 12, 0);
m_statusLabel = new QLabel("就绪");
m_statusLabel->setStyleSheet("color: #ffffff;");
statusLayout->addWidget(m_statusLabel);
statusLayout->addStretch();
QLabel *encoding = new QLabel("UTF-8");
encoding->setStyleSheet("color: #ffffff;");
statusLayout->addWidget(encoding);
QLabel *lang = new QLabel("C++");
lang->setStyleSheet("color: #ffffff; padding-left: 12px;");
statusLayout->addWidget(lang);
mainLayout->addWidget(statusBar);
}
int addTab(const QString &title, const QString &filePath = QString())
{
int index = m_tabBar->addTab(title, filePath);
EditorPage *page = new EditorPage(filePath.isEmpty() ? title : filePath);
connect(page, &EditorPage::contentModified, this, [this, index](bool modified) {
m_tabBar->setTabModified(index, modified);
updateTabTitle(index);
});
m_stack->addWidget(page);
m_tabBar->setCurrentRow(index);
m_stack->setCurrentIndex(index);
updateStatus();
return index;
}
// 关闭标签页
void closeTab(int index)
{
if (index < 0 || index >= m_tabBar->count()) return;
EditorPage *page = qobject_cast<EditorPage*>(m_stack->widget(index));
if (page && page->isModified()) {
// 实际应弹出保存确认对话框
}
m_tabBar->closeTab(index);
QWidget *w = m_stack->widget(index);
m_stack->removeWidget(w);
delete w;
if (m_tabBar->count() > 0) {
int newIndex = qMin(index, m_tabBar->count() - 1);
m_tabBar->setCurrentRow(newIndex);
m_stack->setCurrentIndex(newIndex);
}
updateStatus();
}
void closeOtherTabs(int keepIndex)
{
for (int i = m_tabBar->count() - 1; i >= 0; --i) {
if (i != keepIndex) {
closeTab(i);
}
}
}
void closeRightTabs(int index)
{
for (int i = m_tabBar->count() - 1; i > index; --i) {
closeTab(i);
}
}
void setCurrentIndex(int index)
{
if (index >= 0 && index < m_tabBar->count()) {
m_tabBar->setCurrentRow(index);
m_stack->setCurrentIndex(index);
updateStatus();
}
}
int currentIndex() const
{
return m_tabBar->currentRow();
}
int count() const
{
return m_tabBar->count();
}
EditorPage* currentPage() const
{
return qobject_cast<EditorPage*>(m_stack->currentWidget());
}
private:
void updateTabTitle(int index)
{
QString title = m_tabBar->tabText(index);
bool modified = m_tabBar->item(index)->data(TabIsModifiedRole).toBool();
if (title.startsWith("• ")) {
title = title.mid(2);
}
if (modified && !title.startsWith("• ")) {
m_tabBar->setTabText(index, "• " + title);
} else if (!modified) {
m_tabBar->setTabText(index, title);
}
}
void updateStatus()
{
int count = m_tabBar->count();
if (count == 0) {
m_statusLabel->setText("无打开的文件");
} else {
m_statusLabel->setText(QString("共 %1 个打开的文件").arg(count));
}
}
VSCodeTabBar *m_tabBar;
QStackedWidget *m_stack;
QLabel *m_statusLabel;
signals:
void newTabRequested();
};
#endif // VSCODETABWIDGET_H
三、MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "vscodeTabWidget.h"
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTreeWidget>
#include <QSplitter>
#include <QMenuBar>
#include <QFileDialog>
#include <QMessageBox>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
{
setWindowTitle("VS Code Style Tab Widget - Qt Demo");
resize(1200, 800);
QWidget *central = new QWidget;
setCentralWidget(central);
QHBoxLayout *mainLayout = new QHBoxLayout(central);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
QWidget *activityBar = new QWidget;
activityBar->setFixedWidth(48);
activityBar->setStyleSheet(R"(
QWidget {
background-color: #333333;
}
QPushButton {
background-color: transparent;
color: #858585;
border: none;
font-size: 20px;
padding: 12px;
}
QPushButton:hover {
color: #ffffff;
}
QPushButton:checked {
color: #ffffff;
border-left: 2px solid #007acc;
}
)");
QVBoxLayout *actLayout = new QVBoxLayout(activityBar);
actLayout->setContentsMargins(0, 8, 0, 8);
actLayout->setSpacing(4);
actLayout->setAlignment(Qt::AlignTop);
QStringList icons = {"📁", "🔍", "🌿", "🐛", "📦", "🔧"};
for (const QString &icon : icons) {
QPushButton *btn = new QPushButton(icon);
btn->setFixedSize(48, 48);
btn->setCheckable(true);
actLayout->addWidget(btn);
}
actLayout->addStretch();
mainLayout->addWidget(activityBar);
QWidget *sideBar = new QWidget;
sideBar->setFixedWidth(250);
sideBar->setStyleSheet(R"(
QWidget {
background-color: #252526;
color: #cccccc;
}
)");
QVBoxLayout *sideLayout = new QVBoxLayout(sideBar);
sideLayout->setContentsMargins(0, 0, 0, 0);
sideLayout->setSpacing(0);
QWidget *sideHeader = new QWidget;
sideHeader->setFixedHeight(35);
sideHeader->setStyleSheet("background-color: #252526; border-bottom: 1px solid #333333;");
QHBoxLayout *headerLayout = new QHBoxLayout(sideHeader);
headerLayout->setContentsMargins(12, 0, 12, 0);
QLabel *title = new QLabel("资源管理器");
title->setStyleSheet("color: #bbbbbb; font-size: 11px; font-weight: bold;");
headerLayout->addWidget(title);
sideLayout->addWidget(sideHeader);
QTreeWidget *fileTree = new QTreeWidget;
fileTree->setHeaderHidden(true);
fileTree->setStyleSheet(R"(
QTreeWidget {
background-color: #252526;
color: #cccccc;
border: none;
outline: none;
}
QTreeWidget::item {
height: 22px;
padding-left: 8px;
}
QTreeWidget::item:selected {
background-color: #37373d;
}
QTreeWidget::item:hover {
background-color: #2a2d2e;
}
)");
QTreeWidgetItem *root = new QTreeWidgetItem(fileTree, QStringList() << "VSCodeTabDemo");
root->setExpanded(true);
QStringList files = {
"main.cpp", "vscodeTabBar.h", "vscodeTabWidget.h",
"mainwindow.h", "mainwindow.cpp", "README.md"
};
for (const QString &f : files) {
QTreeWidgetItem *item = new QTreeWidgetItem(root, QStringList() << f);
item->setData(0, Qt::UserRole, "src/" + f);
}
QTreeWidgetItem *inc = new QTreeWidgetItem(root, QStringList() << "include");
new QTreeWidgetItem(inc, QStringList() << "utils.h");
new QTreeWidgetItem(inc, QStringList() << "config.h");
QTreeWidgetItem *res = new QTreeWidgetItem(root, QStringList() << "resources");
new QTreeWidgetItem(res, QStringList() << "icons.qrc");
new QTreeWidgetItem(res, QStringList() << "style.qss");
connect(fileTree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem *item) {
if (item->childCount() == 0) {
QString fileName = item->text(0);
QString filePath = item->data(0, Qt::UserRole).toString();
if (filePath.isEmpty()) filePath = fileName;
for (int i = 0; i < m_tabWidget->count(); ++i) {
if (m_tabWidget->currentPage() &&
m_tabWidget->currentPage()->filePath() == filePath) {
m_tabWidget->setCurrentIndex(i);
return;
}
}
m_tabWidget->addTab(fileName, filePath);
}
});
sideLayout->addWidget(fileTree);
mainLayout->addWidget(sideBar);
m_tabWidget = new VSCodeTabWidget;
connect(m_tabWidget, &VSCodeTabWidget::newTabRequested, this, [this]() {
static int count = 1;
m_tabWidget->addTab(QString("untitled-%1").arg(count++), "");
});
m_tabWidget->addTab("main.cpp", "src/main.cpp");
m_tabWidget->addTab("vscodeTabBar.h", "src/vscodeTabBar.h");
m_tabWidget->addTab("vscodeTabWidget.h", "src/vscodeTabWidget.h");
mainLayout->addWidget(m_tabWidget, 1);
createMenus();
}
private:
void createMenus()
{
QMenuBar *menuBar = new QMenuBar(this);
menuBar->setStyleSheet(R"(
QMenuBar {
background-color: #3c3c3c;
color: #cccccc;
border-bottom: 1px solid #252526;
}
QMenuBar::item {
padding: 6px 12px;
background: transparent;
}
QMenuBar::item:selected {
background-color: #505050;
}
)");
QMenu *fileMenu = menuBar->addMenu("文件");
fileMenu->setStyleSheet(R"(
QMenu {
background-color: #3c3c3c;
color: #cccccc;
border: 1px solid #454545;
}
QMenu::item {
padding: 6px 24px;
}
QMenu::item:selected {
background-color: #094771;
}
)");
QAction *newFile = fileMenu->addAction("新建文件");
QAction *openFile = fileMenu->addAction("打开文件...");
fileMenu->addSeparator();
QAction *saveFile = fileMenu->addAction("保存");
QAction *saveAll = fileMenu->addAction("全部保存");
fileMenu->addSeparator();
QAction *exit = fileMenu->addAction("退出");
connect(newFile, &QAction::triggered, this, [this]() {
static int count = 1;
m_tabWidget->addTab(QString("untitled-%1").arg(count++), "");
});
connect(openFile, &QAction::triggered, this, [this]() {
QString file = QFileDialog::getOpenFileName(this, "打开文件");
if (!file.isEmpty()) {
QFileInfo info(file);
m_tabWidget->addTab(info.fileName(), file);
}
});
connect(saveFile, &QAction::triggered, this, [this]() {
if (m_tabWidget->currentPage()) {
m_tabWidget->currentPage()->setModified(false);
}
});
connect(exit, &QAction::triggered, this, &QMainWindow::close);
QMenu *editMenu = menuBar->addMenu("编辑");
editMenu->addAction("撤销");
editMenu->addAction("重做");
editMenu->addSeparator();
editMenu->addAction("剪切");
editMenu->addAction("复制");
editMenu->addAction("粘贴");
setMenuBar(menuBar);
}
VSCodeTabWidget *m_tabWidget;
};
#endif // MAINWINDOW_H
代码运行结果:
三、小结:
3.1.QContextMenuEvent:
QContextMenuEvent 是 Qt 中专门用于上下文菜单事件(即通常所说的右键菜单事件)的事件类.
3.2.QStackedWidget:
QStackedWidget 是 Qt 中一种堆叠式容器控件,它的核心特点是同一时刻只显示一个子页面,其余页面被隐藏。
典型应用场景
- 配合
QListWidget或QTabBar:实现类似 VS Code 的标签页切换(如你之前的项目) - 配合
QComboBox:实现下拉选择切换不同设置面板 - 向导/分步对话框:配合"上一步/下一步"按钮,按步骤切换页面
- 选项卡式设置界面:左侧列表选择分类,右侧显示对应设置页
与 QTabWidget 的区别
表格
QStackedWidget |
QTabWidget |
|
|---|---|---|
| 标签栏 | 无,需自行实现 | 内置标签栏 |
| 灵活性 | 高,可自由搭配任意切换控件 | 低,样式和行为受限 |
| 使用复杂度 | 需要额外写切换逻辑 | 开箱即用 |
| 适用场景 | 需要自定义标签外观/交互 | 标准选项卡即可满足 |
3.3 自定义角色必须从 Qt::UserRole开始
enum MyRoles {
FilePathRole = Qt::UserRole + 1, // 33
IsModifiedRole, // 34
IsPinnedRole // 35
};
3.4 QStyledItemDelegate
QStyledItemDelegate 是 Qt Model/View 架构中负责自定义数据项外观和编辑行为的核心类。
与 QItemDelegate 的区别
表格
QStyledItemDelegate |
QItemDelegate |
|
|---|---|---|
| 绘制方式 | 使用当前样式表(QStyle)绘制,外观随系统/应用主题变化 | 自行绘制,外观固定 |
| 推荐度 | 首选,Qt 官方推荐 | 旧版兼容,新项目不建议使用 |
| 样式支持 | 完美支持 setStyleSheet() |
样式表支持有限 |
典型使用场景
自定义列表项外观:如你的 VS Code 标签栏项目,通过重写 paint() 实现蓝色指示条、文件图标、关闭按钮等
自定义表格单元格显示:进度条、星级评分、图片预览等
自定义编辑控件:单元格内使用 QComboBox、QDateEdit 等代替默认文本框。
简单例子:
class MyDelegate : public QStyledItemDelegate
{
public:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
// 自定义绘制逻辑
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, QColor("#007acc")); // 选中背景
}
// 绘制文本
QString text = index.data(Qt::DisplayRole).toString();
painter->setPen(Qt::white);
painter->drawText(option.rect, Qt::AlignCenter, text);
}
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
return QSize(100, 30); // 每项固定 100x30
}
};
// 使用
listView->setItemDelegate(new MyDelegate);

浙公网安备 33010602011771号