欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

高仿富途牛牛-组件化(四)-优秀的时钟

程序员文章站 2022-04-29 17:59:14
[TOC] 一、概述 最近一直在仿照富途牛牛做组件化功能,目前已经有了初步的效果。 组件化基础的功能已经有了,接下来就是一些细节上的处理了,比如说加载模板、保存模板、标签页修改名称等等,细节上的问题我们在后续的文章中都会一一做以介绍 最近打算把组件化中的工具箱相关功能做以实现。比如说迷你报价、自选股 ......

目录

一、概述

最近一直在仿照富途牛牛做组件化功能,目前已经有了初步的效果。

组件化基础的功能已经有了,接下来就是一些细节上的处理了,比如说加载模板、保存模板、标签页修改名称等等,细节上的问题我们在后续的文章中都会一一做以介绍

最近打算把组件化中的工具箱相关功能做以实现。比如说迷你报价、自选股、小时钟这些窗口。

仔细观察了牛牛的小窗口,无非就是一个窗口外壳,标题栏和客户区内容,下面我们来具体分析下

1、窗口外壳

例如效果展示中的gif图,小时钟就是一个小窗口,他的外壳对我们肉眼所见到的骑士就是外边线、或者说是高亮选中时候的黄色边框。

窗口外壳是我们进行缩放小窗口的利器,当我们鼠标属于这个外壳时,我们按下鼠标就可以对其进行放大、缩小,如果把鼠标放到四个角进行拖拽的时,你会惊奇的发现他还可以同时朝着两个方向进行缩放

2、标题栏

windows原生的窗口是包含标题栏的,同样他还为我们提供了很好的放大缩小、拖拽、高亮消息通知等很好用的功能。但是windows原生的标题栏属于非客户端,我们是不能直接进行操作的,也就是说我们不能对其进行更多的定制化操作。

这下糟心了,windows原生标题栏用不了了

既然windows原生的标题栏我们用不了,那么我们就只能隐藏原生的标题栏,然后自己去模拟一个新的标题栏,然后实现我们自己需要的所有标题栏该有的行为。

对于这样的小窗口我们已经实现了,在我们之前的文章中也有提到,他就是smallwidget,只是在这个版本的代码里,我们才实现了放大、缩小等功能

3、客户区

理解了上边2个概念,客户区利器起来就很简单了,他就是我们可以操作的区域。

既然标题栏都已经被我们重写了,那么其实我们定制化后的smallwidget窗口,标题栏也就是一个客户区了。

如gif图中所展示的,小时钟窗口上的时间就是客户区内容了。


了解了小窗口这个概念之后,来进入我们本篇文章的重点内容--优秀的时钟

接下来我将会讲述下我们这个小时钟是怎么完成的。

二、效果展示

下面gif图所示,录制的时间比较长,大家可以仔细看下,交互效果完全是参照富途牛牛做的,只是目前视为没有接入正式数据。

如有问题,欢迎提出。感谢!!!

欢迎大家提出问题,交互、配色都可以

高仿富途牛牛-组件化(四)-优秀的时钟

三、小窗口

写这篇文章之前,一直想写一篇小窗口管理的文章,主要是为了更好的通过工具箱来构造小窗口,让使用者使用起来成本更低,无奈架构一直没有写好,因此这里一直往后推。

写这篇文章的时候,小窗口管理的代码结构已经在搭建了,因此展示的代码可能会暴露这一点,但是这不是我们这篇文章讲解的核心,暂时不用关心。

小窗口还是那个窗口类,就像上一篇文章中说的那样,只是这一次我们又加了一些新的功能。

最主要的就是我们重写了鼠标3大事件,用于处理缩放效果

virtual void mousepressevent(qmouseevent * event) override;
virtual void mousemoveevent(qmouseevent *) override;
virtual void mousereleaseevent(qmouseevent * event) override;

这三个函数各司其职,分工协作,完成了缩放事件

  • mousepressevent:记录鼠标按下时的一些状态,比如鼠标按下位置、鼠标按下状态
  • mousemoveevent:负责处理移动事件,判断鼠标是否在窗口边缘,如果在的话,修改鼠标状态
  • mousereleaseevent:当鼠标抬起时,恢复鼠标按下时记录的信息

下面主要分析下第二个步骤,鼠标移动事件处理,他的处理代码可能像下面这样,分为两个部分:修改鼠标状态和修改窗口大小

void smallwidget::mousemoveevent(qmouseevent * event)
{
    if (mleftbuttonpressed)
    {
        resizewidget(event);
    }
    else
    {
        updatecursorshape(event->pos());
    }

    __super::mousemoveevent(event);
}

1、修改鼠标状态

鼠标移动时,当我们发现没有按下鼠标左键,这个时候我们就执行updatecursorshape方法,去判断是否可以进行修改窗口大小,同时修改鼠标状态

当鼠标不在窗口边缘时,也即不满足修改鼠标状态时,记得把鼠标状态还原

a、判断是否满足拖拽

当鼠标处于窗口边缘时,并且在一定的范围内,就认为他进入了修改大小的准备状态

如代码所示,我们首先判断鼠标满足四个边的那几个边的缩放,然后在继续判断是否是在某一个角上拖拽,最后一个变量onedges标志着是否有缩放状态。

当有一个变量onedges变为true时,我们就认为要进行缩放,然后下一步我们就可以进行修改鼠标状态

void smallwidget::recalculate(const qpoint& mousepos, const qrect& framerect)
{
    int mousex = mousepos.x();
    int mousey = mousepos.y();

    int framex = framerect.x();
    int framey = framerect.y();

    int framewidth = framerect.width();
    int frameheight = framerect.height();

    onleftedge = mousex >= framex && mousex <= framex + mborderwidth;//左
    onrightedge = mousex >= framex + framewidth - mborderwidth && mousex <= framex + framewidth;//右 
    ontopedge = mousey >= framey && mousey <= framey + mborderwidth;//上
    onbottomedge = mousey >= framey + frameheight - mborderwidth && mousey <= framey + frameheight;//下
    
    ontopleftedge = ontopedge && onleftedge;
    onbottomleftedge = onbottomedge && onleftedge;
    ontoprightedge = ontopedge && onrightedge;
    onbottomrightedge = onbottomedge && onrightedge;

    //only these checks would be enough
    onedges = onleftedge || onrightedge || ontopedge || onbottomedge;
}

b、修改鼠标状态

判断完是否有边可以进行缩放之后,我们只需要根据这些变量就可以轻易知道,我们要把鼠标状态改成什么样子了。

最后记住,如果不使用了,记得还原鼠标状态

void smallwidget::updatecursorshape(const qpoint & mousepos)
{
    recalculate(mousepos, rect());

    if (ontopleftedge || onbottomrightedge)
    {
        setcursor(qt::sizefdiagcursor);
        mcursorshapechanged = true;
    }
    else if (ontoprightedge || onbottomleftedge)
    {
        setcursor(qt::sizebdiagcursor);
        mcursorshapechanged = true;
    }
    else if (onleftedge || onrightedge)
    {
        setcursor(qt::sizehorcursor);
        mcursorshapechanged = true;
    }
    else if (ontopedge || onbottomedge)
    {
        setcursor(qt::sizevercursor);
        mcursorshapechanged = true;
    }
    else
    {
        if (mcursorshapechanged)//修改鼠标状态
        {
            unsetcursor();
            mcursorshapechanged = false;
        }
    }
}

2、修改大小

修改了鼠标状态实在鼠标未按下的时候触发的,一旦我们修改了鼠标状态后,拖拽的第一步准备工作算是做到位了,这个时候我们只要按下鼠标,继续移动鼠标,然后就进入了修改窗口大小的流程中

修改窗口大小主要是使用了我们鼠标按下时记录的一些信息

窗口大小的量应该等于鼠标按下时到移动的距离偏移,这里我们就那有边框右移来说明问题

假设说右边框本身的值时500,我们鼠标按下时的全局坐标是1000,这个时候鼠标向右移动,移动到了1100这个坐标,那么鼠标其实就是移动了100像素,那么这样就很清晰了,我们的右边框此时的值应该是500+100=600。

为什么移动后的位置 = 从按下时窗口的位置+鼠标按下时到当前位置的偏移量?这样做有一个很大的好处,那就是我们不需要考虑中间的过程,及时中间有些地方处理错了,如果又一次处罚了缩放,那么错误也会被修正。

还有一个好处就是,如果我们每次只做上一次和本次的鼠标位置偏移量,这样处理结果会有异常,根据机器性能,有些机器会丢失需要处理的事件,导致鼠标移动的距离大,我们窗口缩放的少,这个主要是因为qt把很多事件优化了。

void smallwidget::resizewidget(qmouseevent * event)
{
    qpoint mousepos = event->pos();
    qpoint globalpos = event->globalpos();

    qpoint offsetpos = globalpos - m_presspos;

    qrect origrect = m_pressrect;

    if (onleftedge)
    {
        origrect.setleft(origrect.left() + offsetpos.x());
    }
    else if (onrightedge)
    {
        origrect.setright(origrect.right() + offsetpos.x());
    }
    else if (ontopedge)
    {
        origrect.settop(origrect.top() + offsetpos.y());
    }
    else if (onbottomedge)
    {
        origrect.setbottom(origrect.bottom() + offsetpos.y());
    }
    //其他四个角的处理省略
    if (onedges)
    {
        move(origrect.topleft());
        resize(origrect.size());
    }
}

四、时钟窗口

讲完了小窗口,我们的小时钟已经具有了放大、缩小、和移动的功能。

接下来我们分析下这个时钟是怎么显示的,貌似他好像还支持自提自动放大。

首先我们来分析下这个时钟窗口的布局,上边是一个动态刷新的时间文本,下边是一个文本框,主要显示当前日期。仔细看效果展示的gif图,其中主要的应该是上半部分的时间了,因为他居然支持窗口当大时,自身也可以平滑的放大

整个窗口的代码布局,我这里就不做介绍了,比较简单,就是一个垂直布局,其中有一个需要注意的地方就是分割线,研究过qtdesigner的同学应该都知道,qt中的分割线其实就是一个像素的qframe,他的实现代码可能像下面这样,然后加入布局即可

//实现代码
qframe * line = new qframe;
line->setobjectname("line");
line->setfixedheight(1);

//样式表
clocksmall qframe#line{border:1 solid #474f57;}

重要环节登场了,也是我们本篇文章中的核心,支持字体平滑放大,就像效果图那样

void timelabel::paintevent(qpaintevent * event)
{
    __super::paintevent(event);

    qpainter painter(this);
    painter.setrenderhint(qpainter::antialiasing);

    qfont font = painter.font();
    font.setpixelsize(14);
    painter.setfont(font);

    double side = qmin(width(), height());

    int text_w = painter.fontmetrics().width(m_text);
    int text_h = painter.fontmetrics().height();
    
    painter.translate(width() / 2, height() / 2);
    painter.scale(side / text_w * 1.5, side / text_w * 1.5);

    qrect r(-text_w / 2, -text_h / 2, text_w, text_h);
    painter.drawtext(r, m_text);
}

上面这几行代码就是负责绘制时钟的,这里边使用到了一个技巧--场景缩放

painter.scale()这行代码可以把绘制的场景缩放一个比例系数,也就是当我们的窗体放大缩小时,我们根据窗体的大小计算出一个合适的缩放比,然后把场景进行缩放,这样我们的字体自然而然就会变大

绘制时,我们也应该开启平滑绘制qpainter::antialiasing这个属性,这样我们的程序看起来就会更舒畅一些,不会出现很明显的锯齿

在缩放场景的时候,我们是这么干的

  1. 先把中心点平移到窗口中心
  2. 使用缩放比例进行缩放场景
  3. 计算我们字体的宽度和高度
  4. 定义我们字体需要绘制的矩形
  5. 在矩形中绘制我们要显示的文本

这里需要注意一个点,我们必须要计算矩形来绘制文字,如果想计算文字的起始点坐标这个比较困难,因为文字绘制时,并不是说你给的起始点就是文本串的左上角

最后我们做一个定时器,每个一秒进行数据更新,然后刷新界面即可

//启动定时器
qtimer * timer = new qtimer(this);
connect(timer, &qtimer::timeout, this, &clocksmall::updatetime);
timer->start(1000);

updatetime();
    
//更新数据
void clocksmall::updatetime()
{
    qdatetime dt = qdatetime::currentdatetime();
    m_ptime->settext(dt.time().tostring("hh:mm:ss"));

    qstring text = qstringliteral("北京(cn) ") + dt.date().tostring("yyyy/mm/d");
    m_ptext->settext(text);
}

//更新数据 并发起绘制请求
void timelabel::settext(const qstring & text){ m_text = text; update(); }

五、相关文章




转载声明:本站文章无特别说明,皆为原创,版权所有,转载请注明: or twowords