在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

代码运行结果:

ScreenShot_2026-05-07_233546_654

三、小结:

3.1.QContextMenuEvent:

QContextMenuEvent 是 Qt 中专门用于上下文菜单事件(即通常所说的右键菜单事件)的事件类.

3.2.QStackedWidget:

QStackedWidget 是 Qt 中一种堆叠式容器控件,它的核心特点是同一时刻只显示一个子页面,其余页面被隐藏。

典型应用场景

  • 配合 QListWidgetQTabBar:实现类似 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() 实现蓝色指示条、文件图标、关闭按钮等

自定义表格单元格显示:进度条、星级评分、图片预览等

自定义编辑控件:单元格内使用 QComboBoxQDateEdit 等代替默认文本框。

简单例子:

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);
posted @ 2026-05-10 22:37  追风少年X  阅读(11)  评论(0)    收藏  举报