C++实现简化 QtBase(5):通过IEventLoopHost扩展实现win32消息循环

在上一篇文章《C++实现简化版Qt的QObject(4):增加简单实用的事件机制》中,我们实现了普通线程的事件机制。

但是事件机制往往需要和操作系统主线程消息循环一起工作。

因此,今天,我们在之前的CEventLoop的实现之上,通过IEventLoopHost扩展,实现一个windows系统的主线程消息循环扩展,使得我们可以在主线程post task。

CEventLoop 代码

首先,昨天我们写的CEventLoop代码如下:

cpp 复制代码
	class CEventLoop {
	public:
		using Clock = std::chrono::steady_clock;
		using TimePoint = Clock::time_point;
		using Duration = Clock::duration;
		using Handler = std::function<void()>;
		struct TimedHandler {
			TimePoint time;
			Handler handler;
			bool operator<(const TimedHandler& other) const {
				return time > other.time;
			}
		};
		class IEventLoopHost {
		public:
			virtual void onPostTask() = 0;
			virtual void onWaitForTask(std::condition_variable& cond, std::unique_lock<std::mutex>& locker) = 0;
			virtual void onEvent(TimedHandler& event) = 0;
			virtual void onWaitForRun(std::condition_variable& cond, std::unique_lock<std::mutex>& locker, const TimePoint& timePoint) = 0;
		};
	private:
		IEventLoopHost* host = nullptr;
		std::priority_queue<TimedHandler> tasks_;
		std::mutex mutex_;
		std::condition_variable cond_;
		std::atomic<bool> running_{ true };

	public:
		void setHost(IEventLoopHost* host) {
			this->host = host;
		}
		void post(Handler handler, Duration delay = Duration::zero()) {
			std::unique_lock<std::mutex> lock(mutex_);
			tasks_.push({ Clock::now() + delay, std::move(handler) });
			cond_.notify_one();
			if (host) {
				host->onPostTask();
			}
		}

		void run() {
			while (running_) {
				std::unique_lock<std::mutex> lock(mutex_);
				if (tasks_.empty()) {
					if (host) {
						host->onWaitForTask(cond_, lock);
					}
					else {
						cond_.wait(lock, [this] { return !tasks_.empty() || !running_; });
					}
				}

				while (!tasks_.empty() && tasks_.top().time <= Clock::now()) {
					auto task = tasks_.top();
					tasks_.pop();
					lock.unlock();
					if (host) {
						host->onEvent(task);
					}
					else {
						task.handler();
					}

					lock.lock();
				}

				if (!tasks_.empty()) {
					if (host) {
						host->onWaitForRun(cond_, lock, tasks_.top().time);
					}
					else {
						cond_.wait_until(lock, tasks_.top().time);
					}
				}
			}
		}

		void stop() {
			running_ = false;
			cond_.notify_all();
		}

	};

实现扩展

在Windows API中,消息循环通常涉及到调用GetMessageTranslateMessageDispatchMessage函数。以下是一个IEventLoopHost的实现,它将Windows消息循环集成到CEventLoop类中:

cpp 复制代码
#include <Windows.h>

class WindowsEventLoopHost : public CEventLoop::IEventLoopHost {
public:
    // 当任务被投递到事件循环时调用
    void onPostTask() override {
        // 可以使用Windows的PostMessage函数发送一个自定义的消息来唤醒消息循环
        PostThreadMessage(GetCurrentThreadId(), WM_USER, 0, 0);
    }

    // 当事件循环需要等待任务时调用
    void onWaitForTask(std::condition_variable& cond, std::unique_lock<std::mutex>& locker) override {
        locker.unlock();
        MSG msg;
        // 使用PeekMessage而不是GetMessage来避免阻塞,以便定时器事件可以被处理
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        locker.lock();
    }

    // 当事件循环处理事件时调用
    void onEvent(CEventLoop::TimedHandler& event) override {
        // 这里简单地调用事件处理器
        event.handler();
    }

    // 当事件循环需要等待直到特定时间点运行时调用
    void onWaitForRun(std::condition_variable& cond, std::unique_lock<std::mutex>& locker, const CEventLoop::TimePoint& timePoint) override {
        locker.unlock();
        // 计算需要等待的时间
        auto now = CEventLoop::Clock::now();
        auto delay_ms = std::chrono::duration_cast<std::chrono::milliseconds>(timePoint - now).count();
        if (delay_ms > 0) {
            // 设置Windows计时器
            SetTimer(NULL, 0, (UINT)delay_ms, NULL);
            MSG msg;
            // 等待计时器或其他消息
            while (GetMessage(&msg, NULL, 0, 0)) {
                if (msg.message == WM_TIMER) {
                    break; // 计时器消息,继续事件循环
                }
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            KillTimer(NULL, 0);
        }
        locker.lock();
    }
};

在这个实现中,我们通过IEventLoopHost的四个虚函数,完成与Windows消息循环协同工作:

  • onPostTask 使用 PostThreadMessage 来发送一个自定义的消息,以唤醒可能正在等待的消息循环。
  • onWaitForTask 使用 PeekMessage 来处理消息而不阻塞,这样定时器消息可以在适当的时间被处理。
  • onEvent 简单地调用传入的事件处理器,这里我们没有对Windows消息做特别的处理。
  • onWaitForRun 使用 SetTimer 来创建一个Windows计时器,它会在指定的毫秒数后发送一个 WM_TIMER 消息。我们使用 GetMessage 等待这个消息或者其他消息的到来。如果计时器消息到来,我们就终止循环,返回到事件循环,以便继续处理其他任务。

继续优化

在自测过程中进一步的优化代码结构,优化性能,处理WM_QUIT消息,处理一些其他细节等。

最终代码如下:

cpp 复制代码
#include <Windows.h>
class CWindowsEventLoopHost : public base::CEventLoop::IEventLoopHost {
	static constexpr UINT WM_WAKEUP = WM_USER + 15151515;
	UINT_PTR timerID_ = 0;

public:
	~CWindowsEventLoopHost() {
		if (timerID_) {
			KillTimer(NULL, timerID_);
		}
	}

	void onPostTask() override {
		PostThreadMessage(GetCurrentThreadId(), WM_WAKEUP, 0, 0);
	}

	void onWaitForTask(std::condition_variable& cond, std::unique_lock<std::mutex>& locker) override {
		MSG msg;
		while (GetMessage(&msg, NULL, 0, 0)) {
			if (handleWindowsMessage(msg)) {
				break;
			}
			if (cond.wait_for(locker, std::chrono::milliseconds(0), [] { return true; })) {
				break;
			}
		}
	}

	void onEvent(base::CEventLoop::TimedHandler& event) override {
		event.handler();
	}

	void onWaitForRun(std::condition_variable& cond, std::unique_lock<std::mutex>& locker, const std::chrono::steady_clock::time_point& timePoint) override {
		auto now = std::chrono::steady_clock::now();
		if (now < timePoint) {
			auto delay_ms = std::chrono::duration_cast<std::chrono::milliseconds>(timePoint - now).count();
			timerID_ = SetTimer(NULL, 0, (UINT)delay_ms, NULL);//通过timer事件唤醒
			MSG msg;
			while (GetMessage(&msg, NULL, 0, 0)) {
				if (handleWindowsMessage(msg)) {
					break;
				}
			}
		}
	}

	bool handleWindowsMessage(MSG& msg) {
		if (msg.message == WM_QUIT) {
			if (this->eventLoop) {
				this->eventLoop->stop();//退出消息循环
			}
			return true;
		}
		if (msg.message == WM_TIMER && msg.wParam == timerID_) {
			KillTimer(NULL, timerID_);
			timerID_ = 0;
			return true; // Timer event occurred
		}
		TranslateMessage(&msg);
		DispatchMessage(&msg);
		return false;
	}
};

使用示例

由于setHost函数里面没有加锁,而且可能出现在运行过程中被修改host出现不可预期情况。因此,后来去掉setHost函数,改为在CEventLoop的构造函数传入host,避免误用。

使用示例如下:

cpp 复制代码
// 使用示例
int main() {
	CWindowsEventLoopHost host;
	base::CEventLoop loop(&host);

	loop.post([]() {
		std::cout << "Immediate task\n";
		});//马上执行

	loop.post([]() {
		std::cout << "Delayed task\n";
		}, std::chrono::seconds(1));//延时一秒

	loop.post([]() {
		std::cout << "Delayed task in 3 second\n";
		}, std::chrono::seconds(3));//延时3秒

	loop.post([&]() {
		std::cout << "stop msg loop in 6 second\n";
		loop.stop();
		}, std::chrono::seconds(6));//延时6秒

	if (true) {
		loop.run();//主线程直接run
	}
	else {
		// 或者起一个其他线程run:
		std::thread loop_thread([&loop]() {
			loop.run();
			});
		std::this_thread::sleep_for(std::chrono::seconds(10));
		loop.stop();//停止消息循环
		loop_thread.join();
	}
}

完整代码在:https://github.com/kevinyangli/simple_qt_qobject.git

相关推荐
binishuaio几秒前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE2 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻6 分钟前
WPF中的依赖属性
开发语言·wpf
洋24015 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙16 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点17 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
NoneCoder35 分钟前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发
苏三有春35 分钟前
PyQt5实战——UTF-8编码器功能的实现(六)
开发语言·qt
脉牛杂德38 分钟前
多项式加法——C语言
数据结构·c++·算法
legend_jz40 分钟前
STL--哈希
c++·算法·哈希算法