十一、xlib绘制编辑框-续

系列文章目录

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

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



前面的示例我们记录了如何利用xlib提供的接口实现一个编辑框,接受中文字符输入。在这篇文章中我们继续完善编辑框功能。这篇文章准备实现以下功能:插入光标眨眼、支持键盘左键和右键实现实现光标移动,支持鼠标选择显示插入光标点,支持从插入光标处实现插入文字,删除文字(Backspace、Del)。

1.实现插入光标眨眼

结合前一节中我们所实现的定时器功能,我们可以实现一个插入光标眨眼的功能。实现这个功能我们可以把插入光标的显示分为两部分,一部分为显示插入光标。另一部不显示插入光标,显示和不显示各占500ms。这样我们就可以实现一个插入光标的眨眼功能。首先我们需要继承上个示例中的Timeout基类,实现一个定时器事件基类,在Timeout定时器处理事件中我们调用编辑框DoPaint绘制函数。在绘制函数中,我们可以使用一个变量来记录光标显示和不显示的时间,根据这个时间来决定要不要进行插入光标绘制。

处理编辑框的定时器实现类如下:

class EditTimeout : public Timeout {
public:
    EditTimeout(Display *display, Window window,UIEdit &editControl):
        m_editControl{editControl},
    m_display{display},
    m_window{window}
    {

    }
    void OnTimeout() override {
        DoPaint(m_display,m_window,m_editControl);
    }
private:
    UIEdit &m_editControl;
    Display *m_display;
    Window m_window;
};

DoPaint会调用ShowCaret决定要不要进行插入光标绘制。ShowCaret代码实现逻辑修改如下

static uint64_t lastTimeout = 0;

static void ShowCaret(Display *display,Window window, GC gc,UIEdit &editControl) {
    struct timeval tv = {0};
    gettimeofday(&tv,nullptr);
    uint64_t currentTime = tv.tv_sec*1000 + tv.tv_usec/1000;
    if(currentTime-lastTimeout>500 && currentTime-lastTimeout<1000){
        //500~1000毫秒之间不显示插入光标
        return;
    }
    if(currentTime-lastTimeout>=1000){
        lastTimeout = currentTime;
    }
    int textWidth = 0;
    if (editControl.text.length()>0) {
        XGlyphInfo  glyphInfo{0};
        ::XftTextExtentsUtf8(display, editControl.font, reinterpret_cast<const FcChar8 *>(editControl.text.c_str()), editControl.text.length(), &glyphInfo);
        textWidth = glyphInfo.width;
    }
    XCopyArea(display,editControl.m_cursorPixmap,window,gc,0,0,CURSOR_WIDTH,CURSOR_HEIGHT,editControl.x + textWidth + 5,editControl.y+3);
}

当定时器事件处在0-500毫秒之间时,我们显示插入光标,当定时器事件处在500-1000毫秒之间我们不显示插入光标。大于1000时,定时器记录归“零"。还有一个地方需要,那就是在DoPaint函数的开始处,我们需要调用XClearArea清空整个编辑框的位置,因为不清除该区域,当我们不需要显示插入光标时,由于之前绘制的光标可能没有从窗口中清除。DoPaint稍加改动。

void DoPaint(Display *display, Window window,UIEdit &editControl) {
    XClearArea(display,window,editControl.x,editControl.y,editControl.width,editControl.height,False);
    ...
}

在main函数中生成EditTimeout的实例,并且设置每隔100毫秒执行一次定时器事件。将EditTimeout实例添加到TimeoutContext管理的定时器实现中。调用事件循环和非阻塞接口实现事件处理和定时器超时处理。

    EditTimeout *editTimeout = new EditTimeout(display,window,editControl);
    editTimeout->SetInterval(100);//
    TimeoutContext::GetInstance().Add(editTimeout);

    struct pollfd fd = {
        .fd = ConnectionNumber(display),
        .events =  POLLIN
    };
    while (1) {
        bool ret = XPending(display)> 0 || poll(&fd,1,TimeoutContext::GetInstance().GetMinimumTimeout())>0;
        if (!ret) {
            //timeout
            TimeoutContext::GetInstance().ProcessTimeoutEvents();
            continue;
        }
        TimeoutContext::GetInstance().ProcessTimeoutEvents();
        ....
    }

完整代码如下:

#include <clocale>
#include <cstring>
#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sys/poll.h>
#include <X11/Xft/Xft.h>
#include <sys/time.h>

using namespace std;

const int CURSOR_WIDTH = 6;
const int CURSOR_HEIGHT=24;

static unsigned char cursoricon_bits [ ] = {
    0x3f , 0x03f , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c ,
    0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x03f , 0x03f } ;

struct UIEdit {
    int x;
    int y;
    int width;
    int height;
    string text;
    XftFont *font;
    XIM     m_xim;
    XIC     m_xic;
    Pixmap  m_cursorPixmap;
    UIEdit():x{0},
            y{0},
            width{0},
            height{0},
            font{nullptr},
            m_xim{nullptr},
            m_xic{nullptr}
    {

    }
    ~UIEdit() {

    }
};

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);
    }
}

static uint64_t lastTimeout = 0;

static void ShowCaret(Display *display,Window window, GC gc,UIEdit &editControl) {
    struct timeval tv = {0};
    gettimeofday(&tv,nullptr);
    uint64_t currentTime = tv.tv_sec*1000 + tv.tv_usec/1000;
    if(currentTime-lastTimeout>500 && currentTime-lastTimeout<1000){
        //500~1000毫秒之间不显示插入光标
        return;
    }
    if(currentTime-lastTimeout>=1000){
        lastTimeout = currentTime;
    }
    int textWidth = 0;
    if (editControl.text.length()>0) {
        XGlyphInfo  glyphInfo{0};
        ::XftTextExtentsUtf8(display, editControl.font, reinterpret_cast<const FcChar8 *>(editControl.text.c_str()), editControl.text.length(), &glyphInfo);
        textWidth = glyphInfo.width;
    }
    XCopyArea(display,editControl.m_cursorPixmap,window,gc,0,0,CURSOR_WIDTH,CURSOR_HEIGHT,editControl.x + textWidth + 5,editControl.y+3);
}

inline const char* CharNext(const char *p){
    u_char  character = *p;
    if(*p == 0){
        return p;
    }
    if( (character & 0x80) == 0){
        return (p+1);
    }else if( (character >> 5) == 0B110){
        return (p+2);
    }else if( (character>>4) == 0B1110){
        return (p+3);
    }else if( (character>>3) == 0B11110){
        return (p+4);
    }else if( (character>>2) == 0B111110){
        return (p+5);
    }else if( (character>>1) == 0B1111110){
        return (p+6);
    }
    return p+1;
}

inline const char* CharPrev(const char *start, const char *current)
{
    if(start == current){
        return start;
    }
    const char *result = current - 1;
    while(result != start){
        u_char character = *result;
        if( (character>>6) == 0B10){
            result = result - 1;
        }else{
            break;
        }
    }
    return result;
}

void DoPaint(Display *display, Window window,UIEdit &editControl) {
    XClearArea(display,window,editControl.x,editControl.y,editControl.width,editControl.height,False);
    int screen = DefaultScreen(display);
    GC gc = XCreateGC(display,window,0,nullptr);
    XSetForeground(display,gc,0x666666);
    XDrawRectangle(display,window,gc,editControl.x,editControl.y,editControl.width,editControl.height);
    XSetForeground(display,gc, 0xfcfcfc);
    XFillRectangle(display,window,gc,editControl.x+1,editControl.y+1,editControl.width-2,editControl.height-2);
    if (!editControl.text.empty()) {
        XftDraw *xftDraw = XftDrawCreate(display,window,DefaultVisual(display,screen),DefaultColormap(display,screen));
        XftColor    textColor;
        textColor.color.alpha = 0xffff;
        textColor.color.red = 0;
        textColor.color.green = 0;
        textColor.color.blue = 0;
        XftDrawStringUtf8(xftDraw,&textColor,editControl.font,
            editControl.x+3,editControl.y + editControl.font->ascent + 3,
            reinterpret_cast<const FcChar8*>(editControl.text.c_str()),editControl.text.length());
        XftDrawDestroy(xftDraw);
    }
    ShowCaret(display,window,gc,editControl);
    XFreeGC(display,gc);
}

static bool IsPrintableChar(KeySym keysym)
{
    return (keysym>=0x20 && keysym<127) || (keysym>=XK_KP_Multiply && keysym<=XK_KP_9);
}

void DoKeyPress(Display *display, Window window, UIEdit &editControl,XKeyEvent &keyEvent) {
    KeySym keysym = NoSymbol;
    char text[32] = {};
    Status status;
    Xutf8LookupString(editControl.m_xic,&keyEvent,text,sizeof(text)-1,&keysym,&status);
    if(status == XBufferOverflow){
        //an IME was probably used,and wants to commit more than 32 chars.
        //ignore this fairly unlikely case for now
    }
    if(status == XLookupChars){
        editControl.text.append(text);
        DoPaint(display,window,editControl);
    }
    if(status == XLookupBoth){
        if( (!(keyEvent.state & ControlMask)) && IsPrintableChar(keysym))
        {
            editControl.text.append(text);
            DoPaint(display,window,editControl);
        }
        if(keysym == XK_BackSpace){
            if(editControl.text.length()==0){
                return;
            }
            const char *p = editControl.text.c_str() + editControl.text.length();
            //找到以字节为单位的最后一个字符的开始字节处
            const char *charStart = CharPrev(editControl.text.c_str(),p);
            //移除最后一个Unicode字符。
            editControl.text.erase(charStart - editControl.text.c_str(), p-charStart);
            DoPaint(display,window,editControl);
        }
    }
    if(status == XLookupKeySym){
        //a key without text on it
    }
}

class EditTimeout : public Timeout {
public:
    EditTimeout(Display *display, Window window,UIEdit &editControl):
        m_editControl{editControl},
    m_display{display},
    m_window{window}
    {

    }
    void OnTimeout() override {
        DoPaint(m_display,m_window,m_editControl);
    }
private:
    UIEdit &m_editControl;
    Display *m_display;
    Window m_window;
};

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

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "无法打开X显示器\n");
        exit(1);
    }
    setlocale(LC_ALL, "");
    XSetLocaleModifiers("");

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 400, 300, 1,
                                 BlackPixel(display, screen), WhitePixel(display, screen));

    /* 选择要接收的事件类型 */
    XSelectInput(display, window, ExposureMask|KeyPressMask);
    XMapWindow(display, window);

    UIEdit  editControl;
    editControl.x = 30;
    editControl.y = 30;
    editControl.width = 200;
    editControl.height = 40;
    editControl.font = XftFontOpenName(display, screen, "文泉驿微米黑-14");
    editControl.m_xim = XOpenIM(display,0,0,0);
    editControl.m_xic = XCreateIC(editControl.m_xim,XNInputStyle,XIMPreeditNothing|XIMStatusNothing,
                                XNClientWindow,window,
                                XNFocusWindow,window,nullptr);
    editControl.m_cursorPixmap = XCreatePixmapFromBitmapData( display , window ,
                reinterpret_cast<char *>(cursoricon_bits), CURSOR_WIDTH , CURSOR_HEIGHT ,
                0xff000000 , WhitePixel ( display , screen ) ,
                DefaultDepth(display,screen) ) ;
    EditTimeout *editTimeout = new EditTimeout(display,window,editControl);
    editTimeout->SetInterval(100);//
    TimeoutContext::GetInstance().Add(editTimeout);

    struct pollfd fd = {
        .fd = ConnectionNumber(display),
        .events =  POLLIN
    };
    while (1) {
        bool ret = XPending(display)> 0 || poll(&fd,1,TimeoutContext::GetInstance().GetMinimumTimeout())>0;
        if (!ret) {
            //timeout
            TimeoutContext::GetInstance().ProcessTimeoutEvents();
            continue;
        }
        TimeoutContext::GetInstance().ProcessTimeoutEvents();

        XNextEvent(display, &event);
        if(XFilterEvent(&event,None)){
            continue;
        }
        if (event.type == Expose) {
            DoPaint(display,window,editControl);
        }
        if (event.type == KeyPress) {
            DoKeyPress(display,window,editControl,event.xkey);
        }
    }

    TimeoutContext::GetInstance().Remove(editTimeout);
    delete editTimeout;
    XftFontClose(display,editControl.font);
    XDestroyWindow(display, window);
    XCloseDisplay(display);

    return 0;
}

编译代码运行结果如下。这里并不是最终的代码,随着功能的添加,以上部分函数代码可能已经无法满足我们的需求,还需要进行重构。以上给我们的中间临时代码,这也是我们在开发过程中经常遇到的需要对代码进行重构、修改演进。代码很多,实际开发中我们会把以上代码进行重构,根据职责分在不同的头文件和源文件中。

在这里插入图片描述

2.键盘控制插入点

之前所有的示例中。我们的光标插入点都是在编辑框整段文本的最后,比如我们输入20个字符,发现第10个字符出现了错误。按照目前的实现我们需要把第10个字符之后所有输入的字符都删掉。这个显然用户交互体验极差。我们通过键盘中的左键、右键来移动插入光标位置,删除待修改的字符,重新输入。

接下来需要解决的问题一是我们需要修改UIEdit结体体,添加一个能够记录当前光标所在位置变量,这个变量存储的是以“字符”为单位的下标值(变量名为m_caretIndex),当这个变量取值为0时表示我需要在第一个字符之前输入或修改数据,为1时表示在第1个字符之后做插入、修改操作;由于汉字通常占用3个字节、英文占用1个字节;以字符为单位进行操作时,当按下键盘的左、右键、backspace或是del键时,我们只需要光标左移+1、-1、删除前一个字符或删除后一个字符即可。

第二个问题,当我们使用键盘左、右键移动光标后。我们的光标位置发生了改变,这里我们需要重新计算出新的光标点的位置。这里有两种实现方式,一种是在初始化编辑框时我们计算每个字符的宽度,并将所有字符的宽度存储到缓存变量中,当文本发生变化时维护缓冲变量字符宽度时,这种实现方式节省性能,不用每次都计算所有字符宽度,缺点较为复杂,需要维护文本变换时,每个字符对应宽度信息。另一种是当光标信息发生变化时我们就重新计算光标应该显示的位置,我们使用这种方式。

第三个问题,我们想要实现从当光标处向前删除一个字符,或是向后删除一个字符。我们

需要知道m_caretIndex个字符在utf8缓冲区的偏移值 。m_caretIndex对应的utf8字节缓冲区偏移可以将前m_caretIndex每个字符所占的字节相加。

p o s = ∑ i = 0 m _ c a r e t I n d e x − 1 B y t e s   O f   C h a r   A t   i pos = \sum\limits_{i=0}^{m\_caretIndex-1}Bytes\ Of\ Char\ At\ i pos=i=0m_caretIndex1Bytes Of Char At i

上式中的totalBytes即为第m_caretPos个字符在utf8缓冲区中的偏移值。使用代码实现如下:

static int Utf8PosByteCharIndex(int charIndex,UIEdit &editControl) {
    if (charIndex > GetNumberOfCharacters(editControl.text)) {
        charIndex = GetNumberOfCharacters(editControl.text);
    }
    int pos = 0;
    const char *start = editControl.text.c_str();
    const char *end = start + editControl.text.length();
    const char *next = CharNext(start);
    while(charIndex-- && start<end){
        pos += (next - start);
        start = next;
        next = CharNext(start);
    }
    return pos;
}

得到了字符所有utf8缓冲区的偏移后,我们也就可以计算出该光标绘到屏幕后应该显示的位置了

static int CalculateCaretPos(Display *display,UIEdit &editControl) {
    int pos = Utf8PosByteCharIndex(editControl);
    if (pos<0) {
        return 0;
    }
    XGlyphInfo  glyphInfo{0};
    ::XftTextExtentsUtf8(display, editControl.font, reinterpret_cast<const FcChar8 *>(editControl.text.c_str()), pos, &glyphInfo);
    return glyphInfo.width;
}

响应键盘事件,键盘处理事件较为复杂,包括输入文本、删除文字、左键/右键移动光标

输入文本

当我们在显示光标插入点通过键盘输入向文本框中输入文字时,键盘一次性可能输入多个字符,这时m_caretIndex需要加上新输入的字符数。同时我们还需要更新utf8缓冲区,在m_carentIndex所对应的utf8偏移处插入文本。实现逻辑如下:

    if(status == XLookupChars){
        int utf8Pos = Utf8PosByteCharIndex(editControl);
        editControl.text.insert(utf8Pos,text);
        editControl.m_caretIndex += GetNumberOfCharacters(text);
        DoPaint(display,window,editControl);
    }
    if(status == XLookupBoth){
        if( (!(keyEvent.state & ControlMask)) && IsPrintableChar(keysym))
        {
            int utf8Pos = Utf8PosByteCharIndex(editControl);
            editControl.text.insert(utf8Pos,text);
            editControl.m_caretIndex += GetNumberOfCharacters(text);
            DoPaint(display,window,editControl);
            //其它代省略
    }

之前我们直接调用editControl.text.append向文本的尾部追文本。现在我们需要从插入点处插入文本。

删除字符

我们可以使用Backspace从当前插入光标处删除前一个字符,Del删除光标处后面一个字符。下面是我们处理键盘的Backspace和Del事件

    if(status == XLookupBoth){
        if(keysym == XK_BackSpace){
            if(editControl.text.length()==0){
                return;
            }
            //
            const char *p = editControl.text.c_str() + Utf8PosByteCharIndex(editControl);
            //找到以字节为单位的最后一个字符的开始字节处
            const char *charStart = CharPrev(editControl.text.c_str(),p);
            //移除最后一个Unicode字符。
            editControl.text.erase(charStart - editControl.text.c_str(), p-charStart);
            editControl.m_caretIndex--;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_Delete) {
            const char *p = editControl.text.c_str() + Utf8PosByteCharIndex(editControl);
            const char *nextChar = CharNext(p);
            editControl.text-.erase(Utf8PosByteCharIndex(editControl),nextChar-p);
            DoPaint(display,window,editControl);
        }
    }

光标移动

使用left、right、Home、End键实现插入光标位置的移动。

实现逻辑如下

if(status == XLookupKeySym){
        //a key without text on it
        if (keysym == XK_Left) {
            if (editControl.m_caretIndex == 0) {
                return;
            }
            editControl.m_caretIndex--;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_Right) {
            if (editControl.m_caretIndex == editControl.text.length()) {
                return;
            }
            editControl.m_caretIndex++;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_Home) {
            editControl.m_caretIndex = 0;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_End) {
            editControl.m_caretIndex = editControl.text.length();
            DoPaint(display,window,editControl);
        }
    }

一个完整的可运行代码如下

#include <clocale>
#include <cstring>
#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sys/poll.h>
#include <X11/Xft/Xft.h>
#include <sys/time.h>

using namespace std;

const int CURSOR_WIDTH = 6;
const int CURSOR_HEIGHT=24;

static unsigned char cursoricon_bits [ ] = {
    0x3f , 0x03f , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c ,
    0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x0c , 0x03f , 0x03f } ;

struct UIEdit {
    int x;
    int y;
    int width;
    int height;
    string text;
    XftFont *font;
    XIM     m_xim;
    XIC     m_xic;
    Pixmap  m_cursorPixmap;
    int     m_caretIndex;
    UIEdit():x{0},
            y{0},
            width{0},
            height{0},
            font{nullptr},
            m_xim{nullptr},
            m_xic{nullptr},
            m_caretIndex{0}
    {

    }
    ~UIEdit() {

    }
};

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);
    }
}

inline const char* CharNext(const char *p){
    u_char  character = *p;
    if(*p == 0){
        return p;
    }
    if( (character & 0x80) == 0){
        return (p+1);
    }else if( (character >> 5) == 0B110){
        return (p+2);
    }else if( (character>>4) == 0B1110){
        return (p+3);
    }else if( (character>>3) == 0B11110){
        return (p+4);
    }else if( (character>>2) == 0B111110){
        return (p+5);
    }else if( (character>>1) == 0B1111110){
        return (p+6);
    }
    return p+1;
}

inline const char* CharPrev(const char *start, const char *current)
{
    if(start == current){
        return start;
    }
    const char *result = current - 1;
    while(result != start){
        u_char character = *result;
        if( (character>>6) == 0B10){
            result = result - 1;
        }else{
            break;
        }
    }
    return result;
}

static uint32_t GetNumberOfCharacters(const string &str)
{
    const char *start = str.c_str();
    const char *end = str.c_str() + str.length();
    uint32_t result = 0;
    while(start < end){
        start = CharNext(start);
        result++;
    }
    return result;
}

static int Utf8PosByteCharIndex(UIEdit &editControl) {
    int charIndex = editControl.m_caretIndex;
    if (charIndex > GetNumberOfCharacters(editControl.text)) {
        charIndex = GetNumberOfCharacters(editControl.text);
    }
    int pos = 0;
    const char *start = editControl.text.c_str();
    const char *end = start + editControl.text.length();
    const char *next = CharNext(start);
    while(charIndex-- && start<end){
        pos += (next - start);
        start = next;
        next = CharNext(start);
    }
    return pos;
}

static int CalculateCaretPos(Display *display,UIEdit &editControl) {
    int pos = Utf8PosByteCharIndex(editControl);
    if (pos<0) {
        return 0;
    }
    XGlyphInfo  glyphInfo{0};
    ::XftTextExtentsUtf8(display, editControl.font, reinterpret_cast<const FcChar8 *>(editControl.text.c_str()), pos, &glyphInfo);
    return glyphInfo.width;
}

static uint64_t lastTimeout = 0;

static void ShowCaret(Display *display,Window window, GC gc,UIEdit &editControl) {
    struct timeval tv = {0};
    gettimeofday(&tv,nullptr);
    uint64_t currentTime = tv.tv_sec*1000 + tv.tv_usec/1000;
    if(currentTime-lastTimeout>500 && currentTime-lastTimeout<1000){
        //500~1000毫秒之间不显示插入光标
        return;
    }
    if(currentTime-lastTimeout>=1000){
        lastTimeout = currentTime;
    }
    int textWidth = CalculateCaretPos(display,editControl);
    XCopyArea(display,editControl.m_cursorPixmap,window,gc,0,0,CURSOR_WIDTH,CURSOR_HEIGHT,editControl.x + textWidth + 5,editControl.y+3);
}

void DoPaint(Display *display, Window window,UIEdit &editControl) {
    XClearArea(display,window,editControl.x,editControl.y,editControl.width,editControl.height,False);
    int screen = DefaultScreen(display);
    GC gc = XCreateGC(display,window,0,nullptr);
    XSetForeground(display,gc,0x666666);
    XDrawRectangle(display,window,gc,editControl.x,editControl.y,editControl.width,editControl.height);
    XSetForeground(display,gc, 0xfcfcfc);
    XFillRectangle(display,window,gc,editControl.x+1,editControl.y+1,editControl.width-2,editControl.height-2);
    if (!editControl.text.empty()) {
        XftDraw *xftDraw = XftDrawCreate(display,window,DefaultVisual(display,screen),DefaultColormap(display,screen));
        XftColor    textColor;
        textColor.color.alpha = 0xffff;
        textColor.color.red = 0;
        textColor.color.green = 0;
        textColor.color.blue = 0;
        XftDrawStringUtf8(xftDraw,&textColor,editControl.font,
            editControl.x+3,editControl.y + editControl.font->ascent + 3,
            reinterpret_cast<const FcChar8*>(editControl.text.c_str()),editControl.text.length());
        XftDrawDestroy(xftDraw);
    }
    ShowCaret(display,window,gc,editControl);
    XFreeGC(display,gc);
}

static bool IsPrintableChar(KeySym keysym)
{
    return (keysym>=0x20 && keysym<127) || (keysym>=XK_KP_Multiply && keysym<=XK_KP_9);
}

void DoKeyPress(Display *display, Window window, UIEdit &editControl,XKeyEvent &keyEvent) {
    KeySym keysym = NoSymbol;
    char text[32] = {};
    Status status;
    Xutf8LookupString(editControl.m_xic,&keyEvent,text,sizeof(text)-1,&keysym,&status);
    if(status == XBufferOverflow){
        //an IME was probably used,and wants to commit more than 32 chars.
        //ignore this fairly unlikely case for now
    }
    if(status == XLookupChars){
        int utf8Pos = Utf8PosByteCharIndex(editControl);
        editControl.text.insert(utf8Pos,text);
        editControl.m_caretIndex += GetNumberOfCharacters(text);
        DoPaint(display,window,editControl);
    }
    if(status == XLookupBoth){
        if( (!(keyEvent.state & ControlMask)) && IsPrintableChar(keysym))
        {
            int utf8Pos = Utf8PosByteCharIndex(editControl);
            editControl.text.insert(utf8Pos,text);
            editControl.m_caretIndex += GetNumberOfCharacters(text);
            DoPaint(display,window,editControl);
        }
        if(keysym == XK_BackSpace){
            if(editControl.text.length()==0){
                return;
            }
            //
            const char *p = editControl.text.c_str() + Utf8PosByteCharIndex(editControl);
            //找到以字节为单位的最后一个字符的开始字节处
            const char *charStart = CharPrev(editControl.text.c_str(),p);
            //移除最后一个Unicode字符。
            editControl.text.erase(charStart - editControl.text.c_str(), p-charStart);
            editControl.m_caretIndex--;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_Delete) {
            const char *p = editControl.text.c_str() + Utf8PosByteCharIndex(editControl);
            const char *nextChar = CharNext(p);
            editControl.text.erase(Utf8PosByteCharIndex(editControl),nextChar-p);
            DoPaint(display,window,editControl);
        }
    }
    if(status == XLookupKeySym){
        //a key without text on it
        if (keysym == XK_Left) {
            if (editControl.m_caretIndex == 0) {
                return;
            }
            editControl.m_caretIndex--;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_Right) {
            if (editControl.m_caretIndex == editControl.text.length()) {
                return;
            }
            editControl.m_caretIndex++;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_Home) {
            editControl.m_caretIndex = 0;
            DoPaint(display,window,editControl);
        }
        if (keysym == XK_End) {
            editControl.m_caretIndex = editControl.text.length();
            DoPaint(display,window,editControl);
        }
    }
}

class EditTimeout : public Timeout {
public:
    EditTimeout(Display *display, Window window,UIEdit &editControl):
        m_editControl{editControl},
    m_display{display},
    m_window{window}
    {

    }
    void OnTimeout() override {
        DoPaint(m_display,m_window,m_editControl);
    }
private:
    UIEdit &m_editControl;
    Display *m_display;
    Window m_window;
};

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

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "无法打开X显示器\n");
        exit(1);
    }
    setlocale(LC_ALL, "");
    XSetLocaleModifiers("");

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 400, 300, 1,
                                 BlackPixel(display, screen), WhitePixel(display, screen));

    /* 选择要接收的事件类型 */
    XSelectInput(display, window, ExposureMask|KeyPressMask);
    XMapWindow(display, window);

    UIEdit  editControl;
    editControl.x = 30;
    editControl.y = 30;
    editControl.width = 200;
    editControl.height = 40;
    editControl.font = XftFontOpenName(display, screen, "文泉驿微米黑-14");
    editControl.m_xim = XOpenIM(display,0,0,0);
    editControl.m_xic = XCreateIC(editControl.m_xim,XNInputStyle,XIMPreeditNothing|XIMStatusNothing,
                                XNClientWindow,window,
                                XNFocusWindow,window,nullptr);
    editControl.m_cursorPixmap = XCreatePixmapFromBitmapData( display , window ,
                reinterpret_cast<char *>(cursoricon_bits), CURSOR_WIDTH , CURSOR_HEIGHT ,
                0xff000000 , WhitePixel ( display , screen ) ,
                DefaultDepth(display,screen) ) ;
    EditTimeout *editTimeout = new EditTimeout(display,window,editControl);
    editTimeout->SetInterval(100);//
    TimeoutContext::GetInstance().Add(editTimeout);

    struct pollfd fd = {
        .fd = ConnectionNumber(display),
        .events =  POLLIN
    };
    while (1) {
        bool ret = XPending(display)> 0 || poll(&fd,1,TimeoutContext::GetInstance().GetMinimumTimeout())>0;
        if (!ret) {
            //timeout
            TimeoutContext::GetInstance().ProcessTimeoutEvents();
            continue;
        }
        TimeoutContext::GetInstance().ProcessTimeoutEvents();

        XNextEvent(display, &event);
        if(XFilterEvent(&event,None)){
            continue;
        }
        if (event.type == Expose) {
            DoPaint(display,window,editControl);
        }
        if (event.type == KeyPress) {
            DoKeyPress(display,window,editControl,event.xkey);
        }
    }

    TimeoutContext::GetInstance().Remove(editTimeout);
    delete editTimeout;
    XftFontClose(display,editControl.font);
    XDestroyWindow(display, window);
    XCloseDisplay(display);

    return 0;
}

编译以上程序,运行效果如下:

在这里插入图片描述

3.鼠标操作

当然我们也可以通过鼠标点击来确定插入光标的位置。当我们按下鼠标按钮后,需要对编辑框中的文字一个一个进行宽度计算,以确定哪个字符位置与鼠标按下时的位置最近;以此确定插入光标的位置。实现逻辑如下:

if (event.type == ButtonPress) {
            if (event.xbutton.button == Button1) {
                if (event.xbutton.x>=editControl.x && event.xbutton.x<=editControl.x+editControl.width
                    && event.xbutton.y>=editControl.y && event.xbutton.y<=editControl.y+editControl.height) {
                    editControl.m_caretIndex = 0;
                    //鼠标x坐标减去文本开始绘制的位置
                    int textStartX = editControl.x+3;
                    int tmpWidth = abs(event.xbutton.x - textStartX);
                    int pos = -1;
                    int numberOfChar = GetNumberOfCharacters(editControl.text);
                    int currentTextWidth = 0;
                    const char *p = editControl.text.c_str();
                    for (int i=0;i<numberOfChar;i++) {
                        const char *nextChar = CharNext(p);
                        XGlyphInfo  glyphInfo{0};
                        ::XftTextExtentsUtf8(display, editControl.font, reinterpret_cast<const FcChar8*>(p), nextChar-p, &glyphInfo);
                        currentTextWidth += glyphInfo.width;
                        if (abs(event.xbutton.x - (textStartX+currentTextWidth))<tmpWidth) {
                            tmpWidth = abs(event.xbutton.x - (textStartX+currentTextWidth));
                            pos = i;
                        }
                        p=nextChar;
                        //int utf8Pos = Utf8PosByteCharIndex()
                    }
                    if (pos != -1) {
                        editControl.m_caretIndex = pos+1;
                    }
                    DoPaint(display,window,editControl);
                }
            }
        }

编译运行带有鼠标点击功能编辑框。运行结果如下

在这里插入图片描述

以上我们实现了一个复杂的编辑框功能。支持键盘输入、删除、鼠标操作、插入光标显示。当然如果我们仔细查看,上面显示的插入光标会遮盖会一部分文本数据。这时因为插入光标本身是使用位图实现的,占用了6个像素的宽度。所以我们还可以对于编辑框控件进行改进。将文本的显示分成插入光标之前和之后,对于插入光标之后的文本,我们可以在其原本显示位置基础上在x轴方向增加一些像素。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值