用Qt开发GUI应用时,经常需要设置窗口单例显示。趁着最近工作不忙,总结下实现窗口单例的3种常见方法。
- 借助共享内存,QSharedMemory。
- 借助QLocalServer/QLocalSocket。
- 借助DBus框架。
1. 共享内存
共享内存的思路是第一个主窗口初始化时,以关键词key创建一片共享内存。当第二个主窗口初始化时,以同样的key创建共享内存时,就会失败。此时就可以判断主窗口已初始化,第二个主窗口自动退出即可。
这个思路的实现方案已经有一个不错的第三方开源库可以使用,MIT协议,对商业和闭源友好。
GitHub地址如下:
https://github.com/itay-grudev/SingleApplication
2. QLocalServer/QLocalSocket
借助QLocalServer/QLocalSocket实现的思路与共享内存相似,第一个主窗口初始化时以关键词serverName创建一个socke服务。当第二个主窗口初始化时,以同样的serverName创建socket服务时,就会失败。
第二个主窗口可以通过socket服务,将数据发送到第一个主窗口。整个实现逻辑非常简单,不需要继承Q[Core|Gui]Application类。其实本方法就是刚才提到的开源项目的简化。
完整实现代码如下,不超过70行。
#include "SingleApplicationHelper.h"
#include <QLocalSocket>
#include <QDataStream>
#include <QFile>
#include <QDir>
bool SingleApplicationHelper::registerApplication(const QString &appName, Options options, int timeout)
{
QLocalSocket socket;
socket.connectToServer(appName);
if (socket.waitForConnected(timeout)) {
return false;
}
if( options & Mode::User ){
localServer.setSocketOptions(QLocalServer::UserAccessOption );
} else {
localServer.setSocketOptions(QLocalServer::WorldAccessOption );
}
connect(&localServer, &QLocalServer::newConnection, this, &SingleApplicationHelper::slotNewLocalConnect);
// Note: On Unix if the server crashes without closing listen will fail with AddressInUseError.
// To create a new server the file should be removed.
// On Windows two local servers can listen to the same pipe at the same time, but any connections will go to one of the server.
if (!localServer.listen(appName)
&& QAbstractSocket::AddressInUseError == localServer.serverError()) {
// Read Qt sources file, we know the path.
QString socketFile = QDir::tempPath() + '/' + localServer.serverName();
QFile::remove(socketFile);
localServer.listen(appName);
}
return true;
}
bool SingleApplicationHelper::sendSecondApplicationArguments(const QString &appName, const QString &arguments, int timeout)
{
QLocalSocket socket;
socket.connectToServer(appName);
if (!socket.waitForConnected(timeout)) {
return false;
}
QDataStream stream(&socket);
stream.setVersion(QDataStream::Qt_DefaultCompiledVersion);
stream << arguments;
socket.waitForBytesWritten();
return true;
}
void SingleApplicationHelper::slotNewLocalConnect()
{
QLocalSocket *socket = localServer.nextPendingConnection();
if (nullptr != socket) {
socket->waitForReadyRead(500);
QDataStream stream(socket);
stream.setVersion(QDataStream::Qt_DefaultCompiledVersion);
QString args;
stream >> args;
emit sigSecondApplicationArguments(args);
}
}
在项目里集成使用也是非常简单。
#include <QApplication>
#include <QMainWindow>
#include "SingleApplicationHelper.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setApplicationName("MyApp");
QMainWindow w;
SingleApplicationHelper singleAppHelper;
// The first application will register the server successfully, and listen for connections.
// The second try to register the server, but failed.
if (singleAppHelper.registerApplication(a.applicationName(), SingleApplicationHelper::User, 500)) {
QObject::connect(&singleAppHelper, &SingleApplicationHelper::sigSecondApplicationArguments,
&w, &QMainWindow::raise);
w.show();
} else {
// The second application can send the arguments to first application by the method below.
singleAppHelper.sendSecondApplicationArguments(a.applicationName(), "--show", 500);
return 0;
}
return a.exec();
}
项目已经开源,欢迎大家体验,提Bug,哈哈。
https://github.com/ShawZG/SingleApplicationHelper
3. DBus框架
DBus框架是Linux上的一种进程间通信的框架,几乎在所有Linux发行版上都默认集成。但是在Windows上需要先安装DBus框架才可使用此方法。其思路仍然与前两种方法一致。第一个主窗口初始化时,注册DBus服务,第二个主窗口初始化时,以相同参数注册DBus服务,就会失败。
相关代码逻辑如下:
DBusDemo dbusDemo;
QDBusConnection dbus = QDBusConnection::sessionBus();
// 尝试注册D-Bus服务,如果注册成功,表示应用没有启动,启动应用。
// 如果注册失败,则说明应用已经启动了,调用接口,让已启动的应用去完成任务。
if (dbus.registerService("org.example.QDBusDemo")) {
dbus.registerObject("/org/example/QDBusDemo",
&dbusDemo,
QDBusConnection::ExportAllSlots
| QDBusConnection::ExportAllProperties
| QDBusConnection::ExportAllSignals);
// TODO 启动APP等逻辑
} else {
// TODO 通过上述D-Bus方法调用本服务的接口完成业务逻辑
}
之前曾经写过Qt调用DBus相关总结《Qt使用D-Bus几种写法》,感兴趣的可以瞅瞅。