QtSingleapplication单实例-源码分析

QtSingleapplication单实例-源码分析


宋·晏几道- 《临江仙·送钱穆父

梦后楼台高锁,酒醒帘幕低垂。
去年春恨却来时。
落花人独立,微雨燕双飞。

记得小蘋初见,两重心字罗衣。
琵琶弦上说相思。
当时明月在,曾照彩云归。

官方案例 QtSingleApplication分析

1. 描述

这个类允许您创建一次只能运行一个实例的应用程序。也就是说,如果用户尝试启动另一个实例,则会激活已运行的实例。另一种使用场景是客户端-服务器系统,其中第一个启动的实例将承担服务器角色,后续启动的实例将作为该服务器的客户端

默认情况下,使用可执行文件的完整路径来确定两个进程是否是同一应用程序的实例。您也可以提供一个显式的标识符字符串来进行比较。

应用程序应在启动初期创建 QtSingleApplication 对象,并调用 isRunning() 来查明是否已有该应用程序的其他实例正在运行。如果 isRunning() 返回 false,则表示没有其他实例正在运行,且本实例已承担运行实例的角色。在这种情况下,应用程序应继续初始化应用程序用户界面,然后像通常那样通过 exec() 进入事件循环。

当正在运行的应用程序收到来自同一应用程序其他实例的消息时,会发出 messageReceived() 信号。当收到消息时,将应用程序提升至可见状态可能对用户有帮助。为便于实现,QtSingleApplication 提供了 setActivationWindow() 函数和 activateWindow() 槽。

如果 isRunning() 返回 true,则表示已有另一个实例正在运行。可以使用 sendMessage() 函数通知该实例又有另一个实例已经启动。此外,还可以通过此函数将数据(例如用户希望此新实例打开的文件名等启动参数)传递给正在运行的实例。然后,应用程序应当终止(或进入客户端模式)。

如果 isRunning() 返回 true,但 sendMessage() 失败,则表明正在运行的实例可能已无响应。

以下示例展示了如何将现有应用程序转换为使用 QtSingleApplication。这个例子非常简单,并未使用 QtSingleApplication 的所有功能(有关更全面的用法请参考示例)。

    // Original
    int main(int argc, char **argv)
    {
        QApplication app(argc, argv);

        MyMainWidget mmw;
        mmw.show();
        return app.exec();
    }

    // Single instance
    int main(int argc, char **argv)
    {
        QtSingleApplication app(argc, argv);

        if (app.isRunning())
            return !app.sendMessage(someDataString);

        MyMainWidget mmw;
        app.setActivationWindow(&mmw);
        mmw.show();
        return app.exec();
    }

当此 QtSingleApplication 实例被销毁后(通常在进程退出或崩溃时),用户下次尝试运行该应用程序时自然就不会再检测到该实例。下一个调用 isRunning()sendMessage() 的实例将承担新的主运行实例的角色。

对于控制台(非图形界面)应用程序,可以使用 QtSingleCoreApplication 类来替代本类,以避免对 QtGui 库的依赖。

2. 类解析

QtLocalPeer

对于类函数的实现,解析时为了保证逻辑的简单性,做了一些简化。

class QtLocalPeer : public QObject
{
    Q_OBJECT

public:
    QtLocalPeer(QObject *parent = 0, const QString &appId = QString());
    bool isClient();
    bool sendMessage(const QString &message, int timeout);
    QString applicationId() const
        { return id; }

Q_SIGNALS:
    void messageReceived(const QString &message);

protected Q_SLOTS:
    void receiveConnection();

protected:
    QString id;
    QString socketName;
    QLocalServer* server;
    QtLP_Private::QtLockedFile lockFile;

private:
    static const char* ack;
};

QtLocalPeer来确保运行的唯一性。

构造函数的大致逻辑为:

QtLocalPeer::QtLocalPeer(QObject* parent, const QString &appId)
    : QObject(parent), id(appId)
{
    // 1. 生成服务器的连接名称;
    // 2. 生成文件所的名称;
    // 3. 创建并打开读写锁;
    lockFile.setFileName(lockName);
    lockFile.open(QIODevice::ReadWrite);
}

判断当前运行的是服务端还是客户端,一般而言,首次创建为服务端,后面进入的根据文件锁的标记,直接标记为客户端。

bool QtLocalPeer::isClient()
{
    // 首先检查锁文件是否已经被锁定;如果已经被锁定,说明已经有另一个实例在运行,返回 false.用于后续客户端的判断逻辑。快速判断是否已经有实例运行
    if (lockFile.isLocked())
        return false;

    // 尝试获取锁来确认当前实例能否成为主实例。第一次进入时,获取锁成功返回true.
    if (!lockFile.lock(QtLP_Private::QtLockedFile::WriteLock, false))
        return true;
    // 监听 socketName
    bool res = server->listen(socketName);
	
    // 当isRunning()时,可以调用sendMessage()来创建QLocalSocket来绑定和server的连接。
    QObject::connect(server, SIGNAL(newConnection()), SLOT(receiveConnection()));
    return false;
}

已经运行的服务端实例发送消息

bool QtLocalPeer::sendMessage(const QString &message, int timeout)
{
    // 1. 身份验证,如果是主实例,直接返回失败;客户端才行。
    if (!isClient())
        return false;

    // 2. 连接服务器(重试机制)
    QLocalSocket socket;
    socket.connectToServer(socketName);
    connOk = socket.waitForConnected(timeout/2);
        
    // 3. 发送消息给服务端, 同时等待确认报文;
    ds.writeBytes(uMsg.constData(), uMsg.size());
    QByteArray uMsg(message.toUtf8());
    socket.waitForReadyRead(timeout);   // wait for ack
    res = (socket.read(qstrlen(ack)) == ack)

    return res;
}

接受客户端发送的消息,同时转发消息

void QtLocalPeer::receiveConnection()
{
    // 1. 获取客户端连接
    QLocalSocket* socket = server->nextPendingConnection();
    if (!socket)
        return;

    // 2. 等待并读取数据
    socket->waitForReadyRead();
    got = ds.readRawData(uMsgBuf, remaining);
    
    // 3. 发送确认报文 和 转发消息
    socket->write(ack, qstrlen(ack));
    socket->waitForBytesWritten(1000);
    socket->waitForDisconnected(1000); // make sure client reads ack
    delete socket;
    emit messageReceived(message);     //### (might take a long time to return)
}

QtSingleApplication

class QT_QTSINGLEAPPLICATION_EXPORT QtSingleApplication : public QApplication
{
    Q_OBJECT
public:
    QtSingleApplication(int &argc, char **argv, bool GUIenabled = true);
    QtSingleApplication(const QString &id, int &argc, char **argv);

    bool isRunning();
    QString id() const;

    void setActivationWindow(QWidget* aw, bool activateOnMessage = true);
    QWidget* activationWindow() const;

public Q_SLOTS:
    bool sendMessage(const QString &message, int timeout = 5000);
    void activateWindow();

Q_SIGNALS:
    void messageReceived(const QString &message);
private:
    QtLocalPeer *   peer;
    QWidget *actWin;
};
/*!
    如果此应用程序的另一个实例正在运行,则返回 true;否则返回 false。
    此函数不会检测由其他用户运行的此应用程序实例(在 Windows 上:不会检测在其他会话中运行的实例)。
*/
bool QtSingleApplication::isRunning()
{
    return peer->isClient();
}

/*!
    尝试将文本消息 message 发送到当前正在运行的实例。
    当运行中的实例接收到消息时,其 QtSingleApplication 对象会触发 messageReceived() 信号。

    如果消息已发送并被当前实例成功处理,此函数返回 true。
    如果当前没有实例在运行,或者运行中的实例未能在 timeout 毫秒内处理消息,则此函数返回 false。
*/
bool QtSingleApplication::sendMessage(const QString &message, int timeout)
{
    return peer->sendMessage(message, timeout);
}

/*!
    将此应用程序的激活窗口进行还原(如果最小化)、提升至前端并激活。
    如果未设置激活窗口,则此函数不执行任何操作。

    这是一个便捷函数,用于在用户尝试启动另一个实例时向用户显示此应用程序实例已被激活。

    通常应在响应 messageReceived() 信号时调用此函数。默认情况下,如果已设置激活窗口,则会自动执行此操作。
*/
void QtSingleApplication::setActivationWindow(QWidget* aw, bool activateOnMessage)
{
    actWin = aw;
    if (activateOnMessage)
        connect(peer, SIGNAL(messageReceived(const QString&)), this, SLOT(activateWindow()));
    else
        disconnect(peer, SIGNAL(messageReceived(const QString&)), this, SLOT(activateWindow()));
}

3. 实际案例

console

void report(const QString& msg)
{
    qDebug("[%i] %s", (int)QCoreApplication::applicationPid(), qPrintable(msg));
}

void MainClass::handleMessage(const QString &message)
{
    report( "Message received: \"" + message + "\"");
}

int main(int argc, char **argv)
{
    report("Starting up");

    QtSingleCoreApplication app(argc, argv);

    if (app.isRunning()) 
    {
        // (客户端模式)发送消息给当前运行的实例
        QString msg(QString("Hi master, I am %1.").arg(QCoreApplication::applicationPid()));
        bool sentok = app.sendMessage(msg, 2000);
        return 0;
    } 
    else 
    {
        // (服务端模式)
        report("No other instance is running; so I will.");
        MainClass mainObj;
        // 当其他实例发送消息时,mainObj.handleMessage() 会被调用
        QObject::connect(&app, SIGNAL(messageReceived(const QString&)),
                         &mainObj, SLOT(handleMessage(const QString&)));
        return app.exec();
    }
}

trivial

class TextEdit : public QTextEdit
{
    Q_OBJECT
public:
    TextEdit(QWidget *parent = 0)
        : QTextEdit(parent)
    {}
public slots:
    void append(const QString &str)
    {
        QTextEdit::append(str);
    }
};

int main(int argc, char **argv)
{
    // 1. 创建单实例应用
    QtSingleApplication instance(argc, argv);
    
    // 2. 尝试唤醒已运行的实例, 只有客户端进来才可以发送消息给运行实例。
    if (instance.sendMessage("Wake up!"))
		return 0;

    TextEdit logview;
    logview.setReadOnly(true);
    logview.show();

    // 3. 当其他实例发送消息时,自动激活此窗口, 同时接受客户端发送的消息。
    instance.setActivationWindow(&logview);
    QObject::connect(&instance, SIGNAL(messageReceived(const QString&)),
		     &logview, SLOT(append(const QString&)));

    return instance.exec();
}

4. 逻辑流程(AI整理)

QtSingleApplication 完整执行流程图

graph TD A[应用程序启动] --> B[创建QtSingleApplication实例] B --> C[调用isRunning检查] C --> D{是否有实例运行?} D -->|是| E[客户端模式] D -->|否| F[服务端模式] %% 客户端分支 E --> G[构建消息内容] G --> H[调用sendMessage发送消息] H --> I{发送成功?} I -->|是| J[显示发送成功消息] I -->|否| K[显示发送失败消息] J --> L[退出当前实例] K --> L %% 服务端分支 F --> M[创建主窗口界面] M --> N[设置激活窗口setActivationWindow] N --> O[连接messageReceived信号] O --> P[进入事件循环exec] %% 消息处理流程 P --> Q[等待消息] Q --> R[收到messageReceived信号] R --> S[调用激活窗口activateWindow] S --> T[处理具体业务逻辑] T --> Q %% 样式定义 classDef client fill:#e1f5fe classDef server fill:#f3e5f5 classDef message fill:#fff3e0 class E,G,H,I,J,K,L client class F,M,N,O,P,Q,R,S,T server class R,S,T message

详细执行时序图

sequenceDiagram participant User as 用户 participant Client as 客户端实例 participant Server as 服务端实例 participant Lock as 文件锁 participant Socket as 本地Socket Note over User,Socket: 第一次启动(服务端模式) User->>Client: 启动应用 Client->>Lock: 尝试获取文件锁 Lock-->>Client: 获取成功 Client->>Client: 创建主窗口 Client->>Server: 设置为服务端模式 Server->>Socket: 启动监听 Note over Client: 进入事件循环 Note over User,Socket: 第二次启动(客户端模式) User->>Client: 再次启动应用 Client->>Lock: 尝试获取文件锁 Lock-->>Client: 获取失败(锁已被占用) Client->>Socket: 连接到服务端 Socket->>Server: 传递消息 Server->>Server: 激活窗口 Server->>Socket: 返回ACK确认 Socket-->>Client: 收到确认 Client->>Client: 自动退出

文件锁检测流程图

graph TD A[isRunning调用] --> B[检查lockFile.isLocked] B --> C{文件已锁定?} C -->|是| D[返回true: 是客户端] C -->|否| E[尝试lockFile.lockWriteLock, false] E --> F{获取锁成功?} F -->|否| G[返回true: 是客户端] F -->|是| H[启动QLocalServer监听] H --> I{监听成功?} I -->|是| J[返回false: 是服务端] I -->|否| K[返回true: 是客户端] %% 样式定义 classDef client fill:#ffcdd2 classDef server fill:#c8e6c9 class D,G,K client class J server

状态转换图

stateDiagram-v2 [*] --> 应用启动 应用启动 --> 检查单实例: 创建QtSingleApplication 检查单实例 --> 服务端模式: isRunning() = false 检查单实例 --> 客户端模式: isRunning() = true 服务端模式 --> 初始化界面: 创建主窗口 初始化界面 --> 设置激活窗口: setActivationWindow() 设置激活窗口 --> 进入事件循环: exec() 进入事件循环 --> 等待消息: 监听Socket 等待消息 --> 处理消息: messageReceived() 处理消息 --> 激活窗口: activateWindow() 激活窗口 --> 等待消息: 继续监听 客户端模式 --> 发送消息: sendMessage() 发送消息 --> 退出应用: 完成使命 退出应用 --> [*] 服务端模式 --> [*]: 用户关闭应用

Author: Hakuon, 2025-11-30

posted @ 2025-11-30 00:55  Hakuon  阅读(0)  评论(0)    收藏  举报