在Racket中如何实现交互绘图中的可靠实时显示

        以下内容假定读者已经对计算机绘图有基本了解并懂得计算机图形学关于交互绘图的概念。否则可能难于理解,特提醒注意。

1 问题的提出

        用Racket实现GUI程序及绘图操作非常方便而且可以高效完成编程。但是,也同样会遇到对于大图片显示中遇到机器性能不佳造成无法实时交互处理的问题。典型例子是:在一张5M以上的图片(Bitmap)上交互绘制一条直线。在计算机性能不佳时,会发现在交互绘制过程中,图形绘制根本跟不上鼠标光标移动速度。

2 问题分析

        以上问题发生的原因在于交互绘图中的编程处理方式一般为不断重绘来产生连续效果。由于图片大,在不断重绘过程中必然会造成大量的数据转换和显存空间的写入。这里数据转换由程序根据需求自身完成,显存空间写入由系统完成(用语言自带的绘图函数实现)或程序完成(如C语言的显存指针写入),这两者都存在影响实时性的问题。要想实现实时显示的平滑动态绘图效果,一个有效途径就是减少图形数据转换频率和减少图像显存写入频率。 但是我们知道,鼠标光标的移动是连续的,图形库处理鼠标移动事件(在Racket中由"‘motion"事件触发)也是连续的方式获取鼠标光标坐标点(x,y)。最理想的情况是每产生一次"‘motion"事件就进行一次绘制,这样可以做到完全的实时交互显示,但也导致上文的问题——频繁的绘制造成处理时间耗费以致无法跟上鼠标光标的移动速度——理想的实时交互无法完成。因此,解决的办法就是并不需要每次"‘motion"事件都进行绘制响应,保证响应鼠标事件的频率和绘制的消耗时间匹配就可以了。

3 问题的解决

3.1 throttle函数与debounce函数

        网上有一个推荐的办法是采用throttle函数与debounce函数通过限制绘图函数触发频率来解决(参见:函数与debounce函数的详解)。throttle函数在每个delay时间间隔最多只能执行函数一次。debounce函数触发时,使用一个定时器延迟执行操作,当函数被再次触发时,清除已设置的定时器,重新设置定时器;如果上一次的延迟操作还未执行,则会被清除。 throttle函数与debounce函数的区别就是throttle函数在触发后会马上执行,而debounce函数会在一定延迟后才执行。从触发开始到延迟结束,只执行函数一次。 实际上,除了采用这两个函数的思路外,还需解决一个问题——时间间隔该设定成多少的问题。时间间隔太长,绘制过程会产生不连续的情况;时间间隔太短(低于图形绘制的处理时间),同样会出现图形绘制跟不上鼠标光标移动的问题。

3.2 Racket实时绘图显示解决之道

        我们设定一个简单场景来体现要解决的问题:有一个大图片,需要将其中一部分显示在画布上,并在该背景图像上交互绘制一系列线段(line)。 在Racket中,绘图一般在画布canvas%上进行。按一般情况,要实现以上功能,就是开始绘线段后,每产生一次鼠标移动事件,即在画布上重绘一次局部图片("dc%"的"draw-bitmap-section"函数实现),重绘一次已经绘制过的线段("dc%"的"draw-line"实现),重绘当前正在绘制的线段。 在机器性能不够好的情况下,的确如上边所说,显示情况不佳。 以下分几个方面来解决上述问题:

  • 使用timer%对象控制时间来延迟"'motion"事件响应;

  • 用"time"来最取得延迟时间(在"on-size"过程中设定);

  • 用can-draw标记来实现绘图延迟;

  • 通过调整图形绘制方式加快绘图速度。

3.2.1 取得延迟时间

        延迟时间设置的最合理值应该是刚好完成绘制就响应下一个鼠标移动事件,也就是延迟时间等于绘图函数执行时间就可以满足要求。Racket中没有直接提供满足这类型的函数。但有一个函数可以使用——"time"函数——一般用于测试程序耗时多少,但是该函数并不能直接返回响应的值,只针对输出端口输出CPU消耗时间、实际消耗时间、垃圾回收消耗时间三个值(单位为毫秒)。为了取得绘图的时间消耗,我们将输出端口指向字符串端口输出,然后通过提取绘图实际消耗时间(通过正则表达式模式匹配取得),就可以达到我们想要的延迟时间值。 用Racket的宏编写代码如下:

Example:

> (define-syntax-rule (get-time proc)
    (let ([por/old (current-output-port)]
          [por/str (open-output-string)])
      (current-output-port por/str)
      (time proc)
      (define str (get-output-string por/str))
      (current-output-port por/old)
      (string->number
       (car
        (regexp-match
         #rx"[0-9]+"
         (car
          (regexp-match
           #rx"real time: [0-9]+"
           str)))))))

 

3.2.2 can-draw标志的使用

        用can-draw标志来作为是否进行图片绘制的判断值(初始值为"#f")。如can-draw为"#t",则进行绘制;为"#f"不绘制。 在"timer%"时间中断的指定间隔时间后设定为"#t",允许绘制图片;在图片绘制完成后将can-draw设置为"#f"。

(when can-draw
  (on-paint)
  (set! can-draw #f))

3.2.3 "timer%"时钟对象

        在开始交互绘图时创建"timer%"对象,完成交互动作后停止"timer%"对象。

开始:

(new timer%
     [notify-callback
      (lambda ()
        (when (not can-draw)
          (set! can-draw #t)))]
     [interval delay/timer])

结束:

(when (not (void? timer/draw))
  (send timer/draw stop))

3.2.4 更快的图形绘制

        实际项目实践中一般会将已经绘制过的线段直接绘制到图片上,再绘制局部图片,这样显示速度会更快。 以如下函数做示例(其中x/section和y/section为图片片段基点,p/begin和p/end为记录光标两次点击的画布坐标点,详细情况请参考本文末链接的示例程序源代码。):

(define (draw-line-to-source)
  (define dc (send source make-dc))
  (set-line-pen dc)
  (send dc draw-line
        (+ x/section (send p/begin get-x))
        (+ y/section (send p/begin get-y))
        (+ x/section (send p/end get-x))
        (+ y/section (send p/end get-y)))
  (send dc get-bitmap))
 
 
(define (draw-section dc)
  (when (and (not (void? section))
             can-draw)
    (send dc draw-bitmap section 0 0)
    (set! can-draw #f)))

4 示例程序

        本示例程序演示Racket的GUI程序的一个基本内容。其中通过时钟来控制显示频率部分代码标注"==时钟来控制显示频率=="注释标记(共8处)。

注:

1、示例代码在CSDN的资源里可以找到。

2、本文由Racket的Scribble自动生成。

你可能感兴趣的:(Racket,racket,gui,交互绘图,时钟)