十、基于xlib实现定时器

系列文章目录

本系列文章记录在Linux操作系统下,如何在不依赖QT、GTK等开源GUI库的情况下,基于x11窗口系统(xlib)图形界面应用程序开发。之所以使用x11进行窗口开发,是在开发一个基于duilib跨平台的界面库项目,使用gtk时,发现基于gtk的开发,依赖的动态库太多,发布时太麻烦,gtk不支持静态库编译发布。所以我们决定使用更底层的xlib接口。在此记录下linux系统下基于xlib接口的界面开发

一、xlib创建窗口
二、xlib事件
三、xlib窗口图元
四、xlib区域
五、xlib绘制按钮控件
六、绘制图片
七、xlib窗口渲染
八、实现编辑框控件
九、异形窗口
十、基于xlib实现定时器
十一、xlib绘制编辑框-续
十二、Linux实现截屏小工具



无论是在界面程序或是控制台程序中,我们经常需要每隔一段时间做某个事情。不像QT、gtk等高级界面库,xlib中没有提供定时器功能。我们需要自己实现定时功能。通过搜集资料我们知道xlib本身是一种C/S架构,我们使用XOpenDisplay本质是一个客户端向X Server请求连接建立一个连接,我们前面示例中在while循环处理的所有事件都是由X Server产生,然后由客户端进行处理。我们可以通过获取client到X Server连接的文件描述符。通过poll、select、epoll来监测X Server是否有事件产生,而这些接口本身就提供的超时返回功能。我们可以结合网络异步事件模型和X client与X Server连接的文件描述符来实现定时器功能。接下来我们使用介绍如下使用c++、最小堆以及poll来实现定时器功能。

1.定时器基类

首先我们使用c++来定义一个定时器的基类,定时器基类中我们定义定时器的Id、定时器过期时间、定时器执行间隔时间以及定时器过期时执行的函数。基类定义如下

class Timeout
{
public:
    virtual ~Timeout() = default;
    uint32_t GetTimerId(){
        return m_timerId;
    }
    void    SetTimerId(uint32_t id){
        m_timerId = id;
    }

    void SetInterval(uint32_t interval){
        m_interval = interval;
    }

    uint32_t GetInterval(){
        return m_interval;
    }

    uint64_t GetTimeoutMilliseconds(){
        return m_timeoutMilliseconds;
    }

    void    SetTimeoutMilliseconds(uint64_t timeoutMilliseconds){
        m_timeoutMilliseconds = timeoutMilliseconds;
    }

    virtual void    OnTimeout() = 0;

private:
    uint32_t  m_timerId;
    uint32_t  m_interval;
    uint64_t  m_timeoutMilliseconds;
};

2.最小堆

对于定时器事件,一个特点就是我们希望最先到时间点的事件先执行;这看上去使用队列(先进先出)更合适,但是当我插入第一个事件每隔1个小时执行一次,第二个事件每隔5分钟执行一次,这时使用队列就不太合适了;当然这时可以优先级队列;要知道在运行的过程我们可能随时删除某个定时器事件。经过各种考量权衡我们决定可以采用最小堆来存储所有的定时器事件。我们以定时器的过期时间来作为关键字,生成最小堆,即最小堆的第一个元素就是最近要过期的定时器事件。最小堆是一个完全二叉树,这样我们可以采用数组来存储最小堆元素,采用数组的好处是,当我们知道当前节点在数组中的下标值时,可以很容易计算出父节点和左右子节点的下标值。父节点(parentIndex)计算方式如下,假设currentIndex为当前节点下标值。

p a r e n t I n d e x = ( c u r r e n t I n d e x − 1 ) / 2 parentIndex = (currentIndex-1)/2 parentIndex=(currentIndex1)/2

左子节点(leftIndex)下标计算方式如下

l e f t I n d e x = c u r r e n t I n d e x ∗ 2 + 1 leftIndex=currentIndex*2+1 leftIndex=currentIndex2+1

右子节点(rightIndex)下标计算方式如下

r i g h t I n d e x = c u r r e n t I n d e x ∗ 2 + 2 rightIndex = currentIndex*2+2 rightIndex=currentIndex2+2

前面我们之所以要计算节点的父、子下标是因为当我们向堆中插入或删除数据破坏了最小堆的特性时,我们需要进行上移或下移元素来保证最小堆的性质。上移称为ShiftUp操作,下移称为ShiftDown操作。

以下我们使用两种动态图展示最小堆的上移、下移操作

上移(ShiftUp)

向堆中插入一个元素时,我们将新的元素插入到堆中最后一个元素之后。如前所述我们新插入的元素有可能是一个最先被执行的事件,所以这时我们需要执行上移操作。下面是一个最小堆插入元素,上移调整的示例

在这里插入图片描述

下移(ShiftDown)

当我们将堆中的一个元素删除时(通常是移除堆中第一个元素,但我们可以移除堆中任意元素)。我们使用堆中最后一个元素来填充被删除位置的数据,由于最后一个元素填充到被删除元素位置后,可以会破坏最小堆性质,这时我们需要从被删除元素位置开始做下移操作。示例如下

在这里插入图片描述

接下来我们给出一个使用最小堆添加删除定时器事件,调整最小堆的实现代码。

void TimeoutContext::Add(Timeout *timeout) {
    m_timeoutEvents[m_elements] = timeout;
    this->ShiftUp(m_elements++);
}

void TimeoutContext::Remove(Timeout *timeout) {
    if(timeout == nullptr){
        m_timeoutEvents[0] = m_timeoutEvents[--m_elements];
        this->ShiftDown(0);
        return;
    }
    for(uint32_t i=0;i<m_elements;i++){
        if(m_timeoutEvents[i] == timeout){
            m_timeoutEvents[i] = m_timeoutEvents[m_elements-1];
            m_elements--;
            this->ShiftDown(i);
            return;
        }
    }
}

void TimeoutContext::ShiftDown(int currentIndex) {
    int childIndex = currentIndex*2+1;
    while(childIndex < m_elements){
        if(childIndex+1 < m_elements && m_timeoutEvents[childIndex]->GetInterval() > m_timeoutEvents[childIndex+1]->GetTimeoutMilliseconds()){
            //找到两个子节点中,过期时间较小的那个
            childIndex = childIndex + 1;
        }
        if(m_timeoutEvents[currentIndex]->GetTimeoutMilliseconds() <= m_timeoutEvents[childIndex]->GetTimeoutMilliseconds()){
            break;
        }
        Timeout *tmpValue = m_timeoutEvents[currentIndex];
        m_timeoutEvents[currentIndex] = m_timeoutEvents[childIndex];
        m_timeoutEvents[childIndex] = tmpValue;
        currentIndex = childIndex;
        childIndex = currentIndex*2+1;
    }
}

void TimeoutContext::ShiftUp(int currentIndex) {
    int parentIndex = (currentIndex-1)/2;
    while(currentIndex != 0){
        if(m_timeoutEvents[parentIndex]->GetTimeoutMilliseconds()  <= m_timeoutEvents[currentIndex]->GetTimeoutMilliseconds()){
            break;
        }
        Timeout *tmpValue = m_timeoutEvents[parentIndex];
        m_timeoutEvents[parentIndex] = m_timeoutEvents[currentIndex];
        m_timeoutEvents[currentIndex] = tmpValue;
        currentIndex = parentIndex;
        parentIndex = (currentIndex-1)/2;
    }
}

3.xlib事件循环

在xlib事件循环中,我们不能再调用XNextEvent,因为该函数是阻塞调用。如果窗口一直没有事件发生,但是中间却有超时事件发生。XNextEvent就行不通。这时我们可以先使用XPending检查是有事件未处理,XPending是非阻塞调用,用于检查当前连接中未处理事件数;若XPending返回的未处理事件数为0,再调用poll,调用poll一是可以检查连接中是否有事件到达,二是可以为poll设置超时阻塞时间;即超时这个时间poll就会返回;超时时间我们可以取最小堆中的最小元素。实现代码如下:

    struct pollfd fd = {
        .fd = ConnectionNumber(display),
        .events =  POLLIN
    };
    while (glbContinueRunning) {
        bool ret = XPending(display)> 0 || poll(&fd,1,TimeContext::GetInstance().GetMinimumTimeout())>0;
        if (!ret) {
            //timeout
            //处理超时事件
            continue;
        }
        //有XEvent事件的时候,也有可能正好超时事件发生。
        //这里也需要处理下超时事件
     }

4.完整实现代码

基于xlib实现的定时器功能,实现代码稍多。这里给出全部代码,我们把所有代码一次性给出。在正常的开发环境下,可以把以下代码分到多个文件中:如Timeout基类一个头文件,TimeoutContext单独头文件和cpp文件,TimeoutImpl单头文件和cpp文件,main函数单独头文件。

#include <cstdio>
#include <cstdint>
#include <cstring>
#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <sys/time.h>

class Timeout
{
public:
    virtual ~Timeout() = default;
    uint32_t GetTimerId(){
        return m_timerId;
    }
    void    SetTimerId(uint32_t id){
        m_timerId = id;
    }

    void SetInterval(uint32_t interval){
        m_interval = interval;
    }

    uint32_t GetInterval(){
        return m_interval;
    }

    uint64_t GetTimeoutMilliseconds(){
        return m_timeoutMilliseconds;
    }

    void    SetTimeoutMilliseconds(uint64_t timeoutMilliseconds){
        m_timeoutMilliseconds = timeoutMilliseconds;
    }

    virtual void    OnTimeout() = 0;

private:
    uint32_t  m_timerId;
    uint32_t  m_interval;
    uint64_t  m_timeoutMilliseconds;
};


const uint32_t MAX_TIMEOUT_EVENTS = 512;

class TimeoutContext
{
public:
    static TimeoutContext &GetInstance();
    void            Add(Timeout *timeout);
    void            Remove(Timeout *timeout);
    uint64_t        GetMinimumTimeout();
    void            ProcessTimeoutEvents();

private:
    void    ShiftDown(int currentIndex);
    void    ShiftUp(int currentIndex);
private:
    TimeoutContext():m_elements{0}
    {
        memset(m_timeoutEvents,0,sizeof(m_timeoutEvents));
    }
private:
    Timeout *m_timeoutEvents[MAX_TIMEOUT_EVENTS];
    int     m_elements;
};

TimeoutContext &TimeoutContext::GetInstance() {
    static TimeoutContext timeoutContext;
    return timeoutContext;
}

void TimeoutContext::Add(Timeout *timeout) {
    struct timeval now = {0};
    gettimeofday(&now,nullptr);
    uint64_t milliseconds = now.tv_sec*1000 + now.tv_usec/1000;
    timeout->SetTimeoutMilliseconds(milliseconds + timeout->GetInterval());
    m_timeoutEvents[m_elements] = timeout;
    this->ShiftUp(m_elements++);
}

void TimeoutContext::Remove(Timeout *timeout) {
    if(timeout == nullptr){
        m_timeoutEvents[0] = m_timeoutEvents[--m_elements];
        this->ShiftDown(0);
        return;
    }
    for(uint32_t i=0;i<m_elements;i++){
        if(m_timeoutEvents[i] == timeout){
            m_timeoutEvents[i] = m_timeoutEvents[m_elements-1];
            m_elements--;
            this->ShiftDown(i);
            return;
        }
    }
}

void TimeoutContext::ShiftDown(int currentIndex) {
    int childIndex = currentIndex*2+1;
    while(childIndex < m_elements){
        if(childIndex+1 < m_elements && m_timeoutEvents[childIndex]->GetInterval() > m_timeoutEvents[childIndex+1]->GetTimeoutMilliseconds()){
            //找到两个子节点中,过期时间较小的那个
            childIndex = childIndex + 1;
        }
        if(m_timeoutEvents[currentIndex]->GetTimeoutMilliseconds() <= m_timeoutEvents[childIndex]->GetTimeoutMilliseconds()){
            break;
        }
        Timeout *tmpValue = m_timeoutEvents[currentIndex];
        m_timeoutEvents[currentIndex] = m_timeoutEvents[childIndex];
        m_timeoutEvents[childIndex] = tmpValue;
        currentIndex = childIndex;
        childIndex = currentIndex*2+1;
    }
}

void TimeoutContext::ShiftUp(int currentIndex) {
    int parentIndex = (currentIndex-1)/2;
    while(currentIndex != 0){
        if(m_timeoutEvents[parentIndex]->GetTimeoutMilliseconds()  <= m_timeoutEvents[currentIndex]->GetTimeoutMilliseconds()){
            break;
        }
        Timeout *tmpValue = m_timeoutEvents[parentIndex];
        m_timeoutEvents[parentIndex] = m_timeoutEvents[currentIndex];
        m_timeoutEvents[currentIndex] = tmpValue;
        currentIndex = parentIndex;
        parentIndex = (currentIndex-1)/2;
    }
}

uint64_t TimeoutContext::GetMinimumTimeout() {
    if (m_elements == 0) {
        return 0xFFFFFFFF;
    }
    Timeout *timeout = m_timeoutEvents[0];
    struct timeval  now{0};
    gettimeofday(&now, nullptr);
    uint64_t milliseconds = now.tv_sec * 1000 + now.tv_usec/1000;
    return timeout->GetTimeoutMilliseconds() > milliseconds ? timeout->GetTimeoutMilliseconds()-milliseconds :0;
}

void TimeoutContext::ProcessTimeoutEvents() {
    uint32_t processCount = 0;
    //每次循环最多只处理100个超时事件,以防止程序进入死循环状态。
    struct timeval now {0};
    gettimeofday(&now,nullptr);
    uint64_t milliseconds = now.tv_sec*1000 + now.tv_usec/1000;
    while (m_elements>0 && m_timeoutEvents[0]->GetTimeoutMilliseconds()<=milliseconds && processCount++<100) {
        auto *timeout = m_timeoutEvents[0];
        m_timeoutEvents[0] = m_timeoutEvents[--m_elements];
        ShiftDown(0);
        long lRes = 0;
        timeout->OnTimeout();
        this->Add(timeout);
    }
}

class TimeoutImpl :public Timeout{
public:
    void OnTimeout() override;
};

void TimeoutImpl::OnTimeout() {
    printf("TimerId = [%d]  OnTimeout...\n",this->GetTimerId());
}


int main() {
    Display *display;
    Window window;
    int screen;

    /* 打开与X服务器的连接 */
    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "无法打开X显示器\n");
        exit(1);
    }

    screen = DefaultScreen(display);

    /* 创建窗口 */
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 400, 300, 1,
                                 BlackPixel(display, screen), WhitePixel(display, screen));

    /* 显示(映射)窗口 */
    XMapWindow(display, window);

    TimeoutImpl *timeout1 = new TimeoutImpl;
    TimeoutImpl *timeout2 = new TimeoutImpl;
    timeout1->SetTimerId(1);
    timeout1->SetInterval(3000);
    timeout2->SetTimerId(2);
    timeout2->SetInterval(5000);

    TimeoutContext::GetInstance().Add(timeout1);
    TimeoutContext::GetInstance().Add(timeout2);

    struct pollfd fd = {
        .fd = ConnectionNumber(display),
        .events =  POLLIN
    };

    XEvent  event;
    while (1){
        bool ret = XPending(display)> 0 || poll(&fd,1,TimeoutContext::GetInstance().GetMinimumTimeout())>0;
        if (!ret) {
            //timeout
            TimeoutContext::GetInstance().ProcessTimeoutEvents();
            continue;
        }
        //有XEvent事件的时候,也有可能正好超时事件发生。
        TimeoutContext::GetInstance().ProcessTimeoutEvents();
        XNextEvent(display,&event);
    }
    TimeoutContext::GetInstance().Remove(timeout1);
    TimeoutContext::GetInstance().Remove(timeout2);
    XDestroyWindow(display, window);
    XCloseDisplay(display);

    return 0;
}

编译运行以上代码,出现界面后,即使我们没有使用鼠标或键盘操作出现的界面。在控制台中仍然定期打印以下信息

TimerId = [1]  OnTimeout...
TimerId = [2]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [2]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [2]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [2]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [1]  OnTimeout...
TimerId = [2]  OnTimeout...
TimerId = [1]  OnTimeout...

为什么要实现定时器,因为在之前的示例中我们绘制了一个编辑框控件,并且绘制了一个光标。但是这相光标一直是静态显示,为了能够实现插入光标的“眨眼”功能,我们需要每隔一定时间显示或不显示插入光标。这里就会用到定时器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值