在Racket中如何實現交互繪圖中的可靠實時顯示

        以下內容假定讀者已經對計算機繪圖有基本瞭解並懂得計算機圖形學關於交互繪圖的概念。否則可能難於理解,特提醒注意。

問題的提出

        用Racket實現GUI程序及繪圖操作非常方便而且可以高效完成編程。但是,也同樣會遇到對於大圖片顯示中遇到機器性能不佳造成無法實時交互處理的問題。典型例子是:在一張5M以上的圖片(Bitmap)上交互繪製一條直線。在計算機性能不佳時,會發現在交互繪製過程中,圖形繪製根本跟不上鼠標光標移動速度。

問題分析

        以上問題發生的原因在於交互繪圖中的編程處理方式一般爲不斷重繪來產生連續效果。由於圖片大,在不斷重繪過程中必然會造成大量的數據轉換和顯存空間的寫入。這裏數據轉換由程序根據需求自身完成,顯存空間寫入由系統完成(用語言自帶的繪圖函數實現)或程序完成(如C語言的顯存指針寫入),這兩者都存在影響實時性的問題。要想實現實時顯示的平滑動態繪圖效果,一個有效途徑就是減少圖形數據轉換頻率和減少圖像顯存寫入頻率。 但是我們知道,鼠標光標的移動是連續的,圖形庫處理鼠標移動事件(在Racket中由"‘motion"事件觸發)也是連續的方式獲取鼠標光標座標點(x,y)。最理想的情況是每產生一次"‘motion"事件就進行一次繪製,這樣可以做到完全的實時交互顯示,但也導致上文的問題——頻繁的繪製造成處理時間耗費以致無法跟上鼠標光標的移動速度——理想的實時交互無法完成。因此,解決的辦法就是並不需要每次"‘motion"事件都進行繪製響應,保證響應鼠標事件的頻率和繪製的消耗時間匹配就可以了。

問題的解決

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

示例程序

        本示例程序演示Racket的GUI程序的一個基本內容。其中通過時鐘來控制顯示頻率部分代碼標註"==時鐘來控制顯示頻率=="註釋標記(共8處)。

注:

1、示例代碼在CSDN的資源裏可以找到。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章