准備工作
參考 https://wiki.qt.io/Install_Qt_5_on_Ubuntu .
# 安裝g++
sudo apt install build-essential # sudo apt install libfontconfig1 # 安裝openGL支持 sudo apt install mesa-common-dev libglu1-mesa-dev
從ustc鏡像直接下載安裝包, 地址是 http://mirrors.ustc.edu.cn/qtproject/official_releases/qt/5.11/5.11.1/
執行chmod u+x使其可執行
安裝
直接運行 qt-opensource-linux-x64-5.11.1.run, 會出現安裝界面, 這一步可以配置網絡代理, 往下進行需要填寫自己在QT注冊的賬戶和口令, 會進行在線驗證.
設置安裝目錄時, 可以將路徑配置到/opt下, 例如 /opt/qt/Qt5.11.1, 會在最后一步時彈出提示輸入su口令.
組件選擇: 普通使用一個都不用選。全選的話整個安裝下來占地5.4GB
在菜單中找到Qt Creator, 啟動非常快
HiDPI支持
默認安裝時在3K以上高分屏上顯示會有問題,部分界面尺寸及字體非常小,需要配置一下環境變量,修改 .profile,增加
export QT_AUTO_SCREEN_SCALE_FACTOR=1
在Ubuntu20.04, Qt5.14下測試對系統桌面設置為125%的縮放顯示正常。也可以使用 QT_ENABLE_HIGHDPI_SCALING=1
網上搜到的 export QT_DEVICE_PIXEL_RATIO 在運行時會提示warning。
運行一個例子
在網上找了一個例子, 項目名叫First, 但是編譯中出現了錯誤
$ gcc first.o all -o first gcc: error: all: No such file or directory
這個是因為文件名稱first引起了歧義. 將項目名改為MyFirst就正常編譯通過了.
The Makefile contains a generated (fake) target called "first" and it also contains a set of explicit and implied rules related to the source file "first.cpp". The make command builds your TARGET (which is named after the directory if not specified) and then decides that because "first.o" changed it must rebuild the "first" target: at this point it gets confused.
$ make /usr/bin/moc -DQT_NO_DEBUG -DQT_SQL_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_SHARED -I/usr/share/qt4/mkspecs/linux-g++ -I. -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtGui -I/usr/include/qt4/QtSql -I/usr/include/qt4 -I. -I. first.cpp -o first.moc g++ -c -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_SQL_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_SHARED -I/usr/share/qt4/mkspecs/linux-g++ -I. -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtGui -I/usr/include/qt4/QtSql -I/usr/include/qt4 -I. -I. -o first.o first.cpp g++ -Wl,-O1 -Wl,-rpath,/usr/lib/qt4 -o simple_example first.o -L/usr/lib/qt4 -lQtSql -L/usr/lib/mysql -L/usr/lib/qt4 -lQtGui -L/usr/X11R6/lib -lQtCore -lgthread-2.0 -lrt -lglib-2.0 -lpthread gcc first.o all -o first // <<<<< confused, decides to rebuild "first" target using a file called "all" gcc: all: No such file or directory make: *** [first] Error 1
You get similar confusion (that make recovers from) if "TARGET = first" even with no files called "first.cpp".
$ make /usr/bin/moc -DQT_NO_DEBUG -DQT_SQL_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_SHARED -I/usr/share/qt4/mkspecs/linux-g++ -I. -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtGui -I/usr/include/qt4/QtSql -I/usr/include/qt4 -I. -I. main.cpp -o main.moc g++ -c -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_SQL_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_SHARED -I/usr/share/qt4/mkspecs/linux-g++ -I. -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtGui -I/usr/include/qt4/QtSql -I/usr/include/qt4 -I. -I. -o main.o main.cpp make: Circular all <- first dependency dropped. // <<<< confused but recognises the situation g++ -Wl,-O1 -Wl,-rpath,/usr/lib/qt4 -o first main.o -L/usr/lib/qt4 -lQtSql -L/usr/lib/mysql -L/usr/lib/qt4 -lQtGui -L/usr/X11R6/lib -lQtCore -lgthread-2.0 -lrt -lglib-2.0 -lpthread
Adding
.PHONY = first
to Makefile fixes this problem but probably generates another if the TARGET = first. The change would be overwritten by qmake anyway. Easier to just avoid it.
Qt5常用快捷鍵
F2 跳轉到變量的聲明, 切換函數的聲明和實現
F4 在頭文件和C文件之間切換
Ctrl + B 構建
Ctrl + R 運行
Alt + Enter 在H文件的成員變量上, 會右鍵彈出創建setter, getter的選項, 非常省事
Qt5教程
bogotobogo.com的這個教程不錯, 值得推薦 http://www.bogotobogo.com/Qt/Qt_tutorial_list_New.php
一個帶菜單的圖片瀏覽器例子
來源是
http://www.bogotobogo.com/Qt/Qt5_QMainWindow_QAction_ImageViewer.php
http://www.bogotobogo.com/Qt/Qt5_QMainWindow_QAction_ImageViewer_B.php
其中的主要部分代碼為
MenuWindow.pro
#------------------------------------------------- # # Project created by QtCreator 2018-08-27T14:02:44 # #------------------------------------------------- QT += core gui QT += printsupport greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = MenuWindow TEMPLATE = app # The following define makes your compiler emit warnings if you use # any feature of Qt which has been marked as deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS # You can also make your code fail to compile if you use deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ main.cpp \ mainwindow.cpp \ mydialog.cpp HEADERS += \ mainwindow.h \ mydialog.h FORMS += \ mainwindow.ui \ mydialog.ui
mainwindow.h
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QLabel> #include <QScrollArea> #include <QMenu> #include <QAction> #include "mydialog.h" namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void open(); void print(); void zoomIn(); void zoomOut(); void normalSize(); void fitToWindow(); void about(); void newWindow(); private: Ui::MainWindow *ui; MyDialog *myDialog; QLabel *imageLabel; QScrollArea *scrollArea; QMenu *fileMenu; QMenu *viewMenu; QMenu *helpMenu; QAction *openAct; QAction *printAct; QAction *newWindowAct; QAction *exitAct; QAction *zoomInAct; QAction *zoomOutAct; QAction *normalSizeAct; QAction *fitToWindowAct; QAction *aboutAct; QAction *aboutQtAct; double scaleFactor; void updateActions(); void scaleImage(double factor); void adjustScrollBar(QScrollBar *scrollBar, double factor); }; #endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QtWidgets> #include <QFileDialog> #include <QMessageBox> #ifndef QT_NO_PRINTER #include <QPrintDialog> #endif MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); openAct = new QAction(tr("&Open..."), this); openAct->setShortcut(tr("Ctrl+O")); printAct = new QAction(tr("&Print..."), this); printAct->setShortcut(tr("Ctrl+P")); printAct->setEnabled(false); newWindowAct = new QAction(tr("&New Window"), this); newWindowAct->setShortcut(tr("Ctrl+N")); exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcut(tr("Ctrl+Q")); zoomInAct = new QAction(tr("Zoom &In (25%)"), this); zoomInAct->setShortcut(tr("Ctrl+=")); //(Ctrl)(+) zoomInAct->setEnabled(false); zoomOutAct = new QAction(tr("Zoom &Out (25%)"), this); zoomOutAct->setShortcut(tr("Ctrl+-")); //(Ctrl)(-) zoomOutAct->setEnabled(false); normalSizeAct = new QAction(tr("&Normal Size"), this); normalSizeAct->setShortcut(tr("Ctrl+S")); normalSizeAct->setEnabled(false); fitToWindowAct = new QAction(tr("&Fit to Window"), this); fitToWindowAct->setEnabled(false); fitToWindowAct->setCheckable(true); fitToWindowAct->setShortcut(tr("Ctrl+F")); aboutAct = new QAction(tr("&About"), this); aboutQtAct = new QAction(tr("About &Qt"), this); fileMenu = new QMenu(tr("&File"), this); fileMenu->addAction(openAct); fileMenu->addAction(printAct); fileMenu->addAction(newWindowAct); fileMenu->addSeparator(); fileMenu->addAction(exitAct); viewMenu = new QMenu(tr("&View"), this); viewMenu->addAction(zoomInAct); viewMenu->addAction(zoomOutAct); viewMenu->addAction(normalSizeAct); viewMenu->addSeparator(); viewMenu->addAction(fitToWindowAct); helpMenu = new QMenu(tr("&Help"), this); helpMenu->addAction(aboutAct); helpMenu->addAction(aboutQtAct); menuBar()->addMenu(fileMenu); menuBar()->addMenu(viewMenu); menuBar()->addMenu(helpMenu); imageLabel = new QLabel(this); imageLabel->setBackgroundRole(QPalette::Base); imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); imageLabel->setScaledContents(true); scrollArea = new QScrollArea(this); scrollArea->setBackgroundRole(QPalette::Dark); scrollArea->setWidget(imageLabel); setCentralWidget(scrollArea); setWindowTitle(tr("Image Viewer")); resize(500, 400); connect(openAct, SIGNAL(triggered()), this, SLOT(open())); connect(printAct, SIGNAL(triggered()), this, SLOT(print())); connect(exitAct, SIGNAL(triggered()), this, SLOT(close())); connect(zoomInAct, SIGNAL(triggered()), this, SLOT(zoomIn())); connect(zoomOutAct, SIGNAL(triggered()), this, SLOT(zoomOut())); connect(normalSizeAct, SIGNAL(triggered()), this, SLOT(normalSize())); connect(fitToWindowAct, SIGNAL(triggered()), this, SLOT(fitToWindow())); connect(aboutAct, SIGNAL(triggered()), this, SLOT(about())); connect(aboutQtAct, SIGNAL(triggered()), qApp, SLOT(aboutQt())); connect(newWindowAct, SIGNAL(triggered(bool)), this, SLOT(newWindow())); } MainWindow::~MainWindow() { delete ui; } void MainWindow::open() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::currentPath()); if (!fileName.isEmpty()) { QImage image(fileName); if (image.isNull()) { QMessageBox::information(this, tr("Image Viewer"), tr("Cannot load %1.").arg(fileName)); return; } imageLabel->setPixmap(QPixmap::fromImage(image)); scaleFactor = 1.0; printAct->setEnabled(true); fitToWindowAct->setEnabled(true); updateActions(); if (!fitToWindowAct->isChecked()) imageLabel->adjustSize(); } } void MainWindow::print() { } void MainWindow::newWindow() { myDialog = new MyDialog(this); myDialog->resize(300, 200); /*myDialog->setModal(true); myDialog->exec();*/ myDialog->show(); } void MainWindow::zoomIn() { scaleImage(1.25); } void MainWindow::zoomOut() { scaleImage(0.8); } void MainWindow::normalSize() { imageLabel->adjustSize(); scaleFactor = 1.0; } void MainWindow::fitToWindow() { bool fitToWindow = fitToWindowAct->isChecked(); scrollArea->setWidgetResizable(fitToWindow); if (!fitToWindow) { normalSize(); } updateActions(); } void MainWindow::about() { QMessageBox::about(this, tr("About Image Viewer"), tr("<b>Image Viewer</b> example.")); } void MainWindow::updateActions() { zoomInAct->setEnabled(!fitToWindowAct->isChecked()); zoomOutAct->setEnabled(!fitToWindowAct->isChecked()); normalSizeAct->setEnabled(!fitToWindowAct->isChecked()); } void MainWindow::scaleImage(double factor) { Q_ASSERT(imageLabel->pixmap()); scaleFactor *= factor; imageLabel->resize(scaleFactor * imageLabel->pixmap()->size()); adjustScrollBar(scrollArea->horizontalScrollBar(), factor); adjustScrollBar(scrollArea->verticalScrollBar(), factor); zoomInAct->setEnabled(scaleFactor < 3.0); zoomOutAct->setEnabled(scaleFactor > 0.333); } void MainWindow::adjustScrollBar(QScrollBar *scrollBar, double factor) { int value = scrollBar->value(); int step = scrollBar->pageStep(); value = int(factor * value + ((factor - 1) * step/2)); scrollBar->setValue(value); }
QScrollBar組件
結合Qt5中對於QScrollBar的說明, 對void MainWindow::scaleImage(double factor) 和 void MainWindow::adjustScrollBar(QScrollBar *scrollBar, double factor) 這兩個方法進行分析.
a. 這是滾動條的滑塊, 提供了快速的定位, 但是不能提供精確的定位
b. 箭頭按鈕用於精確地瀏覽定位, 對於文本編輯器, 典型的步長是一行, 而對於圖片則是20像素.
c. 翻頁控制部分, 點擊的效果等價於一次翻頁, 正常情況下, 一個翻頁對於scrollbar的value的變化等於滑塊的長度.
每個滾動條有一個value字段, 用於標示此滑塊距離滾動條起點的距離. 這個值一定是介於 minimum() 和 maximum() 之間. 在 minimum value和 maximum value時, 滑塊的位置分別如上圖所示. 所以要注意, 這個滾動條的長度並非等於maximum().
由此可知, 對於前面例子中圖片放大factor倍時, 對於scrollbar.value:
1. 如果value不變, 那么當圖片放大時, 因為maximum value增大, 故滑塊一邊縮小, 一邊位置會往起始方向收縮
2. 如果value與factor進行同等比例的放大, 那么滑塊一邊縮小, 一邊起始邊界的位置保持不變
3. 如果value與factor進行同等比例放大的同時, 增加滑塊長度(pageStep)的收縮長度, 那么滑塊一邊縮小, 一邊中心位置保持不變. 所以上面adjustScrollBar()方法的處理為
value = int(factor * value + ((factor - 1) * step/2));
Qt5的Layout
常用的Layout主要有以下幾種
- QHBoxLayout 將組件按水平進行布局, 寬度平均分割, 高度占滿外框
- QVBoxLayout 將組件按垂直進行布局, 高度平均分割, 寬度占滿外框
- QGridLayout 將組件按表格進行布局, 類似於HTML中的table tr td, 可以水平跨列, 也可以垂直跨行
- QFormLayout 用於有多個成對的標簽+輸入的界面, 可以設置不同風格以及在寬帶不足時自動換行
- QGraphicsAnchorLayout 用於在組件上添加錨點, 並以錨點的相對位置來控制布局
- QStackedLayout 對於內部的組件, 每次只顯示一個(可以用於制作標簽頁)
而在主窗口中, 常用的底層布局並非Layout, 而是QSplitter. 這個自帶了對內部widget的自動拉伸, 以及分隔欄的鼠標拖動調整,非常方便. 可以參考Redis Desktop Manager的布局設計(是qml格式的, 但是可以看出來布局的設計) https://github.com/uglide/RedisDesktopManager/blob/0.9/src/qml/app.qml
import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.1 import QtQuick.Dialogs 1.2 import QtQml.Models 2.2 import QtQuick.Window 2.2 import Qt.labs.settings 1.0 import "." import "./common" import "./common/platformutils.js" as PlatformUtils import "./value-editor" import "./connections-tree" import "./console" import "./server-info" import "./bulk-operations" ApplicationWindow { id: approot visible: true objectName: "rdm_qml_root" title: "Redis Desktop Manager " + Qt.application.version width: 1100 height: 800 property double wRatio : (width * 1.0) / (Screen.width * 1.0) property double hRatio : (height * 1.0) / (Screen.height * 1.0) property var currentValueFormatter Component.onCompleted: { if (hRatio > 1 || wRatio > 1) { console.log("Ratio > 1.0. Resize main window.") width = Screen.width * 0.9 height = Screen.height * 0.8 } if (PlatformUtils.isOSXRetina(Screen)) { bottomTabView.implicitHeight = 100 } } Settings { category: "windows_settings" property alias x: approot.x property alias y: approot.y property alias width: approot.width property alias height: approot.height } SystemPalette { id: sysPalette } FontLoader { id: monospacedFont Component.onCompleted: { source = "qrc:/fonts/Inconsolata-Regular.ttf" } } QuickStartDialog { id: quickStartDialog objectName: "rdm_qml_quick_start_dialog" width: PlatformUtils.isOSX() ? 600 : approot.width * 0.8 } GlobalSettings { id: settingsDialog } ConnectionSettignsDialog { id: connectionSettingsDialog objectName: "rdm_connection_settings_dialog" onTestConnection: { if (connectionsManager.testConnectionSettings(settings)) { hideLoader() showMsg(qsTr("Successful connection to redis-server")) } else { hideLoader() showError(qsTr("Can't connect to redis-server")) } } onSaveConnection: connectionsManager.updateConnection(settings) } MessageDialog { id: notification objectName: "rdm_qml_error_dialog" visible: false modality: Qt.WindowModal icon: StandardIcon.Warning standardButtons: StandardButton.Ok function showError(msg) { icon = StandardIcon.Warning text = msg open() } function showMsg(msg) { icon = StandardIcon.Information text = msg open() } } BulkOperationsDialog { id: bulkOperationDialog } Connections { target: bulkOperations onOpenDialog: { bulkOperationDialog.operationName = operationName bulkOperationDialog.open() } } Connections { target: connectionsManager onEditConnection: { connectionSettingsDialog.settings = config connectionSettingsDialog.open() } onError: { notification.showError(err) } Component.onCompleted: { if (connectionsManager.size() == 0) quickStartDialog.open() } } toolBar: AppToolBar {} BetterSplitView { anchors.fill: parent orientation: Qt.Horizontal BetterTreeView { id: connectionsTree Layout.fillHeight: true Layout.minimumWidth: 350 Layout.minimumHeight: 500 } BetterSplitView { orientation: Qt.Vertical BetterTabView { id: tabs objectName: "rdm_qml_tabs" currentIndex: 0 Layout.fillHeight: true Layout.fillWidth: true Layout.minimumWidth: 650 Layout.minimumHeight: 30 onCurrentIndexChanged: { if (tabs.getTab(currentIndex).tabType) { if (tabs.getTab(currentIndex).tabType == "value") { var realIndex = currentIndex - serverStatsModel.tabsCount(); if (welcomeTab) { realIndex -= 1 } viewModel.setCurrentTab(realIndex); } else if (tabs.getTab(currentIndex).tabType == "server_info") { var realIndex = currentIndex; if (welcomeTab) { realIndex -= 1 } serverStatsModel.setCurrentTab(index); } } } WelcomeTab { id: welcomeTab clip: true objectName: "rdm_qml_welcome_tab" property bool not_mapped: true onClose: tabs.removeTab(index) function closeIfOpened() { var welcomeTab = tabs.getTab(0) if (welcomeTab && welcomeTab.not_mapped) tabs.removeTab(0) } } ServerInfoTabs { model: serverStatsModel } Connections { target: serverStatsModel onRowsInserted: if (welcomeTab) welcomeTab.closeIfOpened() } ValueTabs { objectName: "rdm_qml_value_tabs" model: valuesModel } AddKeyDialog { id: addNewKeyDialog objectName: "rdm_qml_new_key_dialog" width: approot.width * 0.7 height: { if (approot.height > 500) { return approot.height * 0.7 } else { return approot.height } } } Connections { target: valuesModel onKeyError: { if (index != -1) tabs.currentIndex = index notification.showError(error) } onRowsInserted: { if (welcomeTab) welcomeTab.closeIfOpened() } onNewKeyDialog: addNewKeyDialog.open() } } BetterTabView { id: bottomTabView Layout.fillWidth: true Layout.minimumHeight: PlatformUtils.isOSXRetina()? 15 : 30 tabPosition: Qt.BottomEdge Consoles { objectName: "rdm_qml_console_tabs" model: consoleModel } Connections { target: consoleModel onChangeCurrentTab: { bottomTabView.currentIndex = i + 1 } } BetterTab { closable: false title: "Log" icon: "qrc:/images/log.svg" BaseConsole { id: logTab readOnly: true textColor: "#6D6D6E" Connections { target: appLogger onEvent: logTab.append(msg) Component.onCompleted: appLogger.getMessages() } } } } } } }
QT的多國化
在現有代碼中增加多國化的步驟
修改pro文件
增加翻譯配置, 這里可以增加多個
TRANSLATIONS += \ $$PWD/languages/v2rock_zh_CN.ts
生成ts文件
點擊Tools -> External -> Linguist -> Update Translations (lupdate), 會根據上一步的pro文件, 生成對應的ts文件
如果項目中對tr()有改動, 也需要調用這個命令更新ts文件
編輯ts文件
切換到File System模式, 在ts文件上右鍵, 點擊 Open With -> Qt Linguist, 在界面里會列出每一個語言條目, 輸入翻譯后 Ctrl + Enter就會打勾並進入下一條
生成qm文件
可以通過菜單 Tools -> External -> Linguist -> Release Translations (lrelease), 如果項目路徑較復雜, 可以通過命令行執行 lrelease xxx.ts 直接生成
添加qm到資源文件
打開項目資源文件(qrc文件), 將qm文件添加到資源列表, 例如
<!DOCTYPE RCC><RCC version="1.0"> <qresource prefix="/"> <file>images/off.png</file> <file>languages/app_zh_CN.qm</file> </qresource> </RCC>
在程序中引用多國化資源
在main.cpp中, 增加
#include <QTranslator> .... int main(int argc, char *argv[]) { ... QTranslator translate; bool bret = translate.load(":/languages/app_zh_CN.qm"); if(!bret) { qWarning() << "Load language file failed!"; } a.installTranslator(&translate); MainDialog w; w.show(); return a.exec(); }
Qt界面程序編寫的步驟
1. 設計好全局的QAction, 這個可以被頂欄菜單QMenuBar, 工具欄QToolBar 以及 右鍵彈出菜單QMenu引用.
2. 設計好主窗體布局和主要widget
3. 開始制作各種slot, 並且將QAction connect到對應的slot上, 這一步需要開始設計各個彈出的QDialog
一個未完成的主窗口例子
rockredismain.h
#ifndef ROCKREDISMAIN_H #define ROCKREDISMAIN_H #include <QMainWindow> #include <QSplitter> #include <QTreeWidget> #include <QTableWidget> #include <QMenuBar> #include <QMenu> #include <QAction> #include <QToolBar> #include <QLabel> #include <QScrollArea> #include <QVBoxLayout> namespace Ui { class RockRedisMain; } class RockRedisMain : public QMainWindow { Q_OBJECT public: explicit RockRedisMain(QWidget *parent = 0); ~RockRedisMain(); private slots: void onCustomContextMenuRequested(const QPoint& point); void showContextMenu(QTreeWidgetItem* item, const QPoint& globalPos); void onCurrentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); private: Ui::RockRedisMain *ui; QSplitter * splitter; QScrollArea *scrollArea; QTreeWidget *treeWidget; QTableWidget* tableWidget; QMenu *fileMenu; QMenu *helpMenu; QToolBar *fileToolBar; QAction *newConnectionAct; QAction *editConnectionAct; QAction *removeConnectionAct; QAction *exitAct; QAction *aboutAct; void initMenu(); QTreeWidgetItem* addTreeNode(QString name, QString description); QTreeWidgetItem* addChildNode(QTreeWidgetItem *parent, QString name, QString description); }; #endif // ROCKREDISMAIN_H
rockredismain.cpp
#include "rockredismain.h" #include "ui_rockredismain.h" RockRedisMain::RockRedisMain(QWidget *parent) : QMainWindow(parent), ui(new Ui::RockRedisMain) { ui->setupUi(this); initMenu(); splitter = new QSplitter(Qt::Horizontal, this); setCentralWidget(splitter); treeWidget = new QTreeWidget(); treeWidget->setHeaderHidden(true); //treeWidget->setHeaderLabels(QStringList() << "Connections"); treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); QTreeWidgetItem* item = addTreeNode("Folder", "It's a folder"); addChildNode(item, "A Connection Folder", "first"); addChildNode(item, "B Connection File", "second"); item = addTreeNode("File", "it's a file"); addChildNode(item, "A", "first"); addChildNode(item, "B", "second"); addTreeNode("Folder", "It's a folder"); addTreeNode("File", "it's a file"); addTreeNode("Folder", "It's a folder"); addTreeNode("File", "it's a file"); addTreeNode("Folder", "It's a folder"); addTreeNode("File", "it's a file"); treeWidget->resize(100, 100); splitter->addWidget(treeWidget); tableWidget = new QTableWidget(); splitter->addWidget(tableWidget); splitter->setStretchFactor(1,1); setWindowTitle(tr("RockRedis")); resize(800, 600); connect(treeWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenuRequested(QPoint))); connect(treeWidget, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(onCurrentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*))); } RockRedisMain::~RockRedisMain() { delete ui; } void RockRedisMain::initMenu() { fileMenu = new QMenu(tr("&File"), this); helpMenu = new QMenu(tr("&Help"), this); menuBar()->addMenu(fileMenu); menuBar()->addMenu(helpMenu); newConnectionAct = new QAction(tr("&New Connection"), this); newConnectionAct->setShortcut(tr("Ctrl+N")); newConnectionAct->setStatusTip(tr("Add a new connection")); //connect(newConnectionAct, SIGNAL(triggered()), this, SLOT(open())); fileMenu->addAction(newConnectionAct); editConnectionAct = new QAction(tr("&Edit Connection"), this); editConnectionAct->setShortcut(tr("Alt+E")); editConnectionAct->setEnabled(false); //connect(editConnectionAct, SIGNAL(triggered()), this, SLOT(print())); fileMenu->addAction(editConnectionAct); removeConnectionAct = new QAction(tr("Remove Connection"), this); editConnectionAct->setShortcut(tr("Del")); removeConnectionAct->setEnabled(false); //connect(newWindowAct, SIGNAL(triggered(bool)), this, SLOT(newWindow())); fileMenu->addAction(removeConnectionAct); exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcut(tr("Ctrl+Q")); connect(exitAct, SIGNAL(triggered()), this, SLOT(close())); fileMenu->addAction(exitAct); aboutAct = new QAction(tr("&About"), this); helpMenu->addAction(aboutAct); } QTreeWidgetItem* RockRedisMain::addTreeNode(QString name, QString description) { QTreeWidgetItem *item = new QTreeWidgetItem(treeWidget, 0); item->setText(0, name); item->setText(1, description); return item; } QTreeWidgetItem* RockRedisMain::addChildNode(QTreeWidgetItem *parent, QString name, QString description) { QTreeWidgetItem *item = new QTreeWidgetItem(parent, 1); item->setText(0, name); item->setText(1, description); return item; } void RockRedisMain::onCustomContextMenuRequested(const QPoint& point) { QTreeWidgetItem* item = treeWidget->itemAt(point); if (item) { // Note: We must map the point to global from the viewport to // account for the header. showContextMenu(item, treeWidget->mapToGlobal(point)); } } void RockRedisMain::showContextMenu(QTreeWidgetItem* item, const QPoint& point) { QMenu menu; switch (item->type()) { case 0: menu.addAction(editConnectionAct); menu.addAction(removeConnectionAct); break; case 1: menu.addAction("This is a type 2"); break; } menu.exec(point); } void RockRedisMain::onCurrentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) { if (current == NULL) { editConnectionAct->setEnabled(false); removeConnectionAct->setEnabled(false); } else if (current->type() == 0) { editConnectionAct->setEnabled(true); removeConnectionAct->setEnabled(true); } else { editConnectionAct->setEnabled(false); removeConnectionAct->setEnabled(false); } }
QT項目的圖標
在Linux下發布項目, 圖標遵循的也是Linux下的規范, 現在較流行的是 freedesktop , 可以參考其最新的文檔. 應用的圖標必須按theme分類, theme下面再按圖標尺寸分目錄存儲. 必須實現的theme是hicolor. 在Ubuntu20.04下, 圖標的存放位置主要為 /usr/share/icons/ 和 $HOME/.local/share/icons/ 這兩個目錄, 其他的在/usr/share/ 下分應用, 在各自目錄下還有一些icons目錄.
/usr/share/icons/ 是系統主要的圖標目錄, 下面有hicolor, Yaru, locolor, default, gnome等多個目錄, 每個目錄對應的一種theme, 在每個theme目錄下, 是按尺寸分類的子目錄, 每個子目錄下是對應尺寸的應用圖標類別, 類別下才是圖標的png文件. 16x16這個尺寸的圖標是最完整的.
/usr/share/icons/hicolor/16x16/apps/$ ll total 164 -rw-r--r-- 1 root root 658 Mar 13 16:32 bluetooth.png -rw-r--r-- 1 root root 1418 Mar 24 22:05 display-im6.q16.png -rw-r--r-- 1 root root 526 Apr 15 22:27 evolution-mail.png -rw-r--r-- 1 root root 553 Apr 15 22:27 evolution-memos.png -rw-r--r-- 1 root root 714 Apr 15 22:27 evolution-tasks.png -rw-r--r-- 1 root root 459 Feb 8 18:59 filezilla.png -rw-r--r-- 1 root root 722 May 8 22:42 firefox.png -rw-r--r-- 1 root root 411 Mar 19 23:30 gcr-gnupg.png -rw-r--r-- 1 root root 693 Mar 19 23:30 gcr-key-pair.png -rw-r--r-- 1 root root 511 Mar 19 23:30 gcr-key.png -rw-r--r-- 1 root root 267 Mar 19 23:30 gcr-password.png -rw-r--r-- 1 root root 334 Mar 19 23:30 gcr-smart-card.png -rw-r--r-- 1 root root 528 May 1 15:10 gnome-power-manager.png -rw-r--r-- 1 root root 781 May 1 15:10 goa-panel.png -rw-r--r-- 1 root root 689 May 10 01:30 google-chrome.png -rw-r--r-- 1 root root 640 Apr 2 19:25 hook-notifier.png -rw-r--r-- 1 root root 508 Mar 30 17:50 ibus-keyboard.png -rw-r--r-- 1 root root 586 Apr 16 19:44 inkscape.png
同樣需要了解的是freedesktop的桌面應用圖標標准, https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
這個標准規定了桌面圖標文件(.desktop文件)的格式. desktop文件通常存儲於 /usr/share/applications/ 和 $HOME/.local/share/applications/ 目錄. 前者是系統的應用入口文件路徑, 可以被所有用戶訪問. 以下是QT自己的desktop文件作為例子
[Desktop Entry] Type=Application Exec="/opt/qt/Qt5.14.2/Tools/QtCreator/bin/qtcreator" %F Name=Qt Creator GenericName=The IDE of choice for Qt development. Icon=QtProject-qtcreator StartupWMClass=qtcreator Terminal=false Categories=Development;IDE;Qt; MimeType=text/x-c++src;text/x-c++hdr;text/x-xsrc;application/x-designer;applicat ion/vnd.qt.qmakeprofile;application/vnd.qt.xml.resource;text/x-qml;text/x-qt.qml ;text/x-qt.qbs;
查看QT生成的AppImage文件
有兩種類型,
Type 1:
mkdir mountpoint sudo mount -o loop my.AppImage mountpoint/ # You can now inspect the contents # You can now also copy the contents to a writable location of your hard disk sudo umount mountpoint/ # Do not forget the umount step! # If you do forget it, your system may exhibit unwanted behavior.
Type 2:
mkdir mountpoint my.AppImage --appimage-offset 123456 # This is just an example output sudo mount my.AppImage mountpoint/ -o offset=123456 # you can now inspect the contents sudo umount mountpoint/ # Do not forget the umount step! # If you do forget it, your system may exhibit unwanted behavior.