Lisp-Stat 翻譯 —— 第八章 繪圖窗體

第八章 繪圖窗體

    Lisp-Stat提供的統計繪圖其基礎是由兩個級別組成的。較低的級別將由本章描述,它提供了在窗體裏繪製線與圖形的工具,還有對用戶動做產生的時間作出響應。第二級別,會在下一章描述,它增長了處理數據的能力。第二章裏繪製的全部圖形都是從下一章裏描述的高級的原型裏繼承來的。若是你對定製一些標準圖形感興趣,你能夠簡略地瀏覽本章並移步下章內容。然而,儘管你不須要直接使用本章所述的細節,你也會發現理解支持高級繪圖對象的基本的繪圖模型是頗有用的。 算法

8.1 繪圖模式

    在窗體裏,經過改變圖片元素也就是像素的顏色在窗體裏繪製的對象叫圖像。繪圖模型提供了一個高級的,考慮繪圖過程的統一框架。隱藏在Lisp-Stat繪圖系統之下的繪圖模型從本質上講是Macintosh系統的QuickDraw模型的一個簡化版本,再與SunView系統和X11系統的一些特徵的組合。 canvas

8.1.1 座標、繪圖模式和顏色

    繪圖窗體裏的繪製操做發生在一個概念性的繪圖畫布裏。該畫布的維度能夠是可變的也能夠是固定的。若是維度可變,那麼畫布與窗體內容是相同的。若是維度是固定的,那麼窗體僅展現了畫布的子矩形視圖。在這種狀況下,能夠經過使用滾動條將窗體固定到畫布裏。 數組

    

圖8.1 柵格顯示器上的座標系統 app

    在柵格顯示器上,顯示其上的最小單元叫像素,假設像素是正方形,所以它們的寬度天然成爲測量屏幕長度的基本單元。當咱們在屏幕上畫一個點的時候,咱們能夠用來顯示該點的最小顯示方式就是一個像素。由於表示一個距離零點的實體點的數學語法是(x,y),咱們須要一個約定,用來關聯像素與數學座標系統。圖8.1展現了一個疊加到像素點集上的座標系統。對於一個繪圖窗口,畫布的左上角就是座標系的原點。x軸(或者說橫軸)以一個像素爲單位向右增加;y軸(或者說縱軸)以一個像素爲單位向下增加,這與一般的數學約定是相反的,可是這是在像素層面用來表示柵格圖形的標準系統。使用左上角座標,咱們就能夠飲用像素了。所以,像素(2,3)表示(2,3), (3,3), (3,4), (2,4)這4個頂點組成的矩形像素。 框架

    每一個像素均可以使用不一樣的顏色繪製。在單色顯示器上,可用的顏色只有黑色和白色。在彩色顯示器上其它顏色也是可用的。在大多數彩色顯示器上,可用的顏色包括黑色、白色、紅色、綠色、藍色、青色、洋紅色和黃色。初始狀況下,窗體裏全部像素的顏色是相同的,都是背景色。當你在窗體裏繪圖時,你能夠將指定像素的顏色改變成要繪製的顏色。擦除操做指擦除指定對象,好比說矩形,將使用背景色代替組成對象的像素。塗色操做將使用繪圖顏色代替對象的像素顏色。 dom

    背景色和繪圖色是繪圖系統的狀態,你能夠在任什麼時候間改變他們,新的顏色將一直有效直到你再次改變他們。改變當前背景色或者繪圖色不會直接影響到任何像素。它只會對未來的繪圖操做的結果又影響。每一個窗體都有它本身的繪圖狀態集。 編輯器

    繪圖系統的另外一個狀態時繪圖模式。繪圖模式能夠是正常模式和XOR(異或)模式中的一種。在正常模式裏,對一個像素繪製操做的影響與該像素前一個顏色是獨立的。在XOR(異或)模式裏,對一個像素着色將反轉像素的顏色。當繪製黑白顏色時,它是由良好定義的:若是像素是黑色,它將變成白色;若是像素是白色,它將變成黑色。在彩色繪圖裏該結果就是不可預測的了,可是即便在彩色顯示器上,XOR模式也有以下特性:繪製一個對象兩次,所以會反轉組成它的對象兩次,屏幕會恢復成它的原始狀態。該特性使得XOR繪圖成爲一個有用的動畫工具,由於它容許你再不須要強制保存和恢復背景的前提下,在背景上邊移動一個對象。在Lisp-Stat繪圖裏,XOR繪圖方法用來實現畫刷矩形。 函數

    繪圖系統的最後兩個狀態時線類型和線寬。直線是使用畫筆繪製的,畫筆是方形像素集合,並與座標軸平行,可視爲從一個點移動到另外一個點,將墨水描繪在它接觸到的像素上。畫筆的尺寸是由線寬指定的。畫筆能夠以不一樣的風格和類型繪圖。畫筆類型有兩種:實線和虛線。 工具

8.1.2 可繪圖對象

    Lisp-Stat繪圖系統提供了高級繪圖程序,用來衆多不一樣的對象,好比說矩形、字符和位圖。 字體

矩形、橢圓形和弧形

    一些基本的繪圖程序能夠處理矩形。矩形就是像素的直角集合,它們的邊與屏幕邊緣平行。矩形使用4個數的術語來指定:它的左上角的兩個座標,寬度,還有高。這四個數字就是一個矩形的座標。矩形能夠被擦除、塗色或者當作框架。當一個矩形被當作框架的時候,畫筆將沿着矩形邊緣的內側運動。當前繪圖模式和線型是可使用。爲矩形塗色就是將它的像素的顏色設置爲繪圖顏色;擦除矩形就是將它的像素的顏色設置爲背景色。

    這裏的橢圓形是指長軸和短軸都平行於座標軸的橢圓。它們能夠經過其包絡矩形來指定。橢圓形的弧線由橢圓形的包絡矩形和兩個角度來指定,這兩個角度是起始角度和增量。起始角度以橫軸開始,向你始終方向爲量度。增量就是從起始角度到弧度末端的角度,單位以度數制計。橢圓形和弧形也能夠做爲框架、能夠塗色和刪除。

多邊形

    多邊形是由整數對的列表指定的,表示多邊形的頂點座標。默認地,這些座標被解釋爲相對於原點的絕對座標。它們也能夠解釋成相對座標,這種狀況下座標集的第二個元素表示與第一個元素的相對偏移量,第三個元素表示相對於第二個元素的相對偏移量,以此類推。多邊形能夠被框選、填充和擦除。對於填充和擦除操做,若是第一個點和最後一個點不相等的話,則在它們之間畫一條弦。框選操做不會閉合一個多邊形,若是你想框選一個閉合的多邊形,你必須確保第一個和最後一個頂點是相等的。

符號、字符串和位圖

    字符與點狀符號是由位圖構成的,即黑白像素的矩形集合。實例如圖8.2。經過將那些位圖疊加到窗口上來繪製字符。在正常的繪圖模式下,在字符黑色部分下邊的窗體像素將由繪圖顏色上色,白色部分不變。在異或繪圖模式下,黑色部分下的像素顏色反轉。

    繪圖符號是小位圖對,一個用於高亮點,一個用於非高亮的點。在正常模式下用來繪製符號的規則與用來繪製字符的規則略有不一樣:在符號的白色區域的下邊的窗體像素使用背景色進行上色,這確保了經過在老的符號上繪製一個新的符號的方式,將一個高亮的符號替換爲一個非高亮的符號。Lisp-Stat提供的符號大小是不一樣的,它們多是三個、四個或者五個像素寬度。

    符號位圖是爲了快速繪圖而專心設計和實現的。你也可使用0和1組成的二維數組來手工繪製矩形位圖,這裏的0表示白色,1表示黑色。位圖影像也能夠經過位圖蒙版的方式來實現。只有那些與蒙版裏相對應的像素才受繪圖操做的影響。在正常繪圖模式下,包含在蒙版裏的影像的黑色像素使用繪圖顏色來繪製,白色像素使用背景色繪製。對應使用蒙版的方式繪製字符的規則與字符影響是相同的;使用蒙版繪製符號的規則就是一個包裹符號影像的正方形。

8.2 繪圖消息

8.2.1初始化和狀態消息

    基礎繪圖常涉及到的原型是graph-window-proto,經過向該原型發送:new消息,能夠建立新的窗體。該原型的:isnew方法不須要任何參數,可是能夠提供標準的window關鍵字:title, :location, :size和:go-away,窗體的默認標題是"Graph Window",go-away的默認值是t,還有一些其它的關鍵字也是可用的。使用:menu關鍵字能夠建立帶菜單的窗體,關鍵字:black-on-white, :has-v-scroll和:has-h-scroll用來設置初始化的繪圖顏色和背景顏色,而且肯定窗體是否有水平滾動條和垂直滾動條。默認地,繪圖顏色是黑色,背景顏色是白色,窗體無滾動條。:isnew方法將在屏幕上顯示一張圖形,除非:show關鍵字的值爲nil。表達式

> (setf w (send graph-window-proto :new))
構造了一個全部選項爲默認設置的窗體。

    窗體的繪圖心痛可使用:back-color, :draw-color, :draw-mode, :line-width和:line-type消息來肯定和設置。

> (send w :back-color)
WHITE
> (send w :draw-color)
BLACK
> (send w :draw-mode)
NORMAL
> (send w :line-width)
1
> (send w :line-type)
SOLID
    這些訪問消息帶一個可選參數用來設置新的狀態,線寬必須是一個正整數,繪圖模式指定爲符號normal或者xor中的一個。咱們剛剛建立的窗體的繪圖模式可使用下邊的語句改變成xor模式,在改回normal模式:
> (send w :draw-mode 'xor)
XOR
> (send w :draw-mode 'normal)
NORMAL
>
    線型能夠指定爲solid或者dashed,顏色能夠是有color-symbols函數返回的列表中的符號中的任意一個,在黑白單色顯示器上惟一可使用的顏色是黑色和白色;在彩色顯示器上可用的符號包括black, white, red, green, blue, cyan, magenta和yellow.

    色彩相關命令不會起任何做用,除非窗體正在使用彩色繪圖,:use-color消息能夠用來肯定是否處於這種狀況。經過分別給定一個可選參數true或者nil,它也能夠用來指示窗體是否使用彩色。對於咱們的窗體:

> (send w :use-color)
NIL
    初始狀況下,窗體使用黑色和白色繪圖。若是你的系統支持彩色,可使用下面的表達式告訴窗體使用彩色繪圖:
> (send w :use-color t)
T
在單色顯示器上出白色之外的全部顏色都被當成黑色。

    消息:reverse-color偶爾是頗有用的,它交替地改變當前繪圖顏色和背景色,而且發送:redraw消息給窗體。

8.2.2 基礎繪圖消息

    爲了在一個窗體裏繪製一個單獨的像素,你能夠向窗體對象發送:draw-point消息,該消息帶兩個整型參數即該點的座標。例如,下式用來將位置爲(15,10)座標位置的像素上色成繪圖顏色:

> (send w :draw-point 15 10)
NIL

    爲了繪製一條線,你能夠向窗體對象發送一個帶4個整型參數的:draw-line消息,直線開始點和結束點的x、y座標值,表達式以下,它繪製了一條從(5,10)到(50,70)的直線:

> (send w :draw-line 5 10 50 70)
NIL
    矩形可使用:fram-rect, :erase-rect和:paint-rect消息來繪製。這些消息都須要四個整形參數,即矩形左上角的x、y座標,還有矩形的長與寬。下邊的表達式繪製了一個寬度爲100像素,高度爲50像素的矩形圖文框,它的左上角座標正好是咱們剛纔畫的哪條直線的終點。
> (send w :frame-rect 50 70 100 50)
NIL
    內接於矩形的橢圓形使用:fram-oval, :erase-oval和:paint-oval消息來繪製。這些消息須要四個整型參數來指定包絡矩形。咱們可使用如下表達式將一個實心的橢圓形放置到矩形下側10像素位置處:
> (send w :paint-oval 50 130 100 50)
NIL
    圓弧形可使用:frame-arc, :paint-arc和:erase-arc消息來繪製。這些消息都須要6個參數,前四個參數是描述弧形所在的橢圓的包絡矩形,剩下的兩個參數是實數,用來指定弧形的開始和步進角度。一個開始角度爲30°並在水平線之上增量爲45°的弧形能夠這樣加到咱們的窗體裏:
> (send w :paint-arc 50 70 100 50 30 45)
NIL
    多邊形可使用:frame-poly, :paint-poly和:erase-poly來繪製。這些消息須要一個表示頂點的整型數值對列表的列表。默認地,這些座標對會被解譯成相對於原點的座標。若是使用的可選參數的值爲nil,那麼除了第一個座標以外其它座標都是相對前一個點的相對座標。咱們可使用:erase-poly在矩形上邊繪製一個三角形:
> (send w :frame-poly '((50 50) (150 50) (100 10) (50 50)))
NIL
該表達式使用了絕對座標,與其等價的相對座標的表達式是這樣的:
> (send w :frame-poly '((50 50) (100 0) (-50 -40) (-50 40)) nil)
NIL
這些繪圖操做積累到一塊兒的結果見圖8.3。

圖8.3 基礎繪圖窗口

    在大多數系統上,若是你使用另外一個窗體覆蓋窗體w,而後再移走,繪製的圖形將消失——窗體沒有記憶它的內容。第二章裏描述的標準繪圖函數重畫了繪圖內容,這樣不管何時從新調整大小或者窗體遮蓋部分從新露出來都會重繪。8.4節將描述如何確保在須要的時候,窗口會重畫。

8.2.3 額外的消息

    窗體的所有內容能夠經過向窗體發送:erase-window消息來擦除,爲了擦除窗體內容,咱們可使用以下表達式:

> (send w :erase-window)
NIL
    與繪製一條定長度的直線不一樣,你可能想要繪製一條從窗體左上角到窗體中心的直線,爲了作到這一點,你須要可以肯定窗體畫布的寬度與高度,:canvas-width和:canvas-height這兩個消息將返回這些信息。該直線能夠這樣繪製:
> (let ((x (round (/ (send w :canvas-width) 2)))
        (y (round (/ (send w :canvas-height) 2))))
    (send w :draw-line 0 0 x y))
NIL
    :draw-string和:draw-string-up兩個消息在窗體裏水平地或者垂直地繪製一個字符串,這兩個消息帶一個字符串和兩個整型數據爲參數,這兩個整型數表明字符串繪製點的x、y座標。如下表達式在點(100,120)處繪製了字符串"Hello"。

    爲了可以定位一個字符串相對於一個座標刻度的位置,咱們須要肯定字符串的大小。:text-string消息帶一個字符串爲參數,並返回它是多少像素的寬度;:text-ascent消息不帶參數,返回一個字符在窗體字體基線以上的最大上行高度(注:ascen上行高度:從原點到字體中最高,這裏的高深都是以基線爲參照線的,的字形的頂部的距離,ascent是一個正值),單位爲像素;消息:text-descent返回相對基線如下的最小下行高度。那麼下邊的表達式將垂直地繪製一個字符串"World",挨着字符串"Hello"的右側。

> (let ((ta (send w :text-ascent))
        (tw (send w :text-width "Hello")))
    (send w :draw-string-up "World" (+ 100 ta tw) 120))
NIL
    不少狀況下,你可使用:draw-text消息繪製字符串而避免計算起始點,這個消息的方法帶一個字符串和4個數字做爲參數。前兩個數字時一個點座標,第三個數應該是0、1或2,分別表示字符串是否應該左對齊,居中對齊或右對齊,最後一個參數應該是0或1,表示字符串是否應該畫在點的上邊或下標。:draw-text-up消息與之相同,不過旋轉90°。

    :draw-symbol消息在指定點繪製了一個符號,該符號使用兩個參數指定,一個符號名和一個表示真假的標記,若是符號是高亮的,該標記就是true,不然爲nil。符號名應該是由函數plot-symbol-symgols返回的類表的符號之中的一個,這個列表至少包含如下符號,dot, dot1, dot2, dot3, dot4, disk, diamond, cross, square, wedge1 wedge2和x。例如,如下兩個表達式繪製了標準點狀符號的常規版本和高亮版本,這些符號在第二章裏用過了。

> (send w :draw-symbol 'disk nil 100 100)
NIL
> (send w :draw-symbol 'disk t 100 120)
NIL
    :replace-symboel消息在指定位置用另外一個符號代替原有的符號。爲了在(100,100)點處,用一個高亮的圓盤狀符號代替一個常規的圓盤狀符號,你可使用以下表達式:

> (send w :replace-symbol 'disk nil 'disk t 100 100)
NIL
該表達式將檢測在用新符號替換前是否有老符號要擦除。

    :draw-bitmap消息用來繪製位圖,這個消息的方法須要3個參數:該位圖和表示放置該位圖的左上角座標的兩個整型數。位圖自己應該是一個0、1矩陣。例如,下邊的表達式將在窗體裏繪製一個小十字。

> (send w :draw-bitmap
        '#2a((0 1 0) (1 1 1) (0 1 0)) 100 140)
NIL
:draw-bitmap消息也可能接收4個參數,即便用另外一個位圖矩陣做爲蒙版。該矩陣的維度應該與影響位圖的維度相同,默認蒙版是一個全1的矩陣。

練習 8.1

8.3 雙緩衝和動畫

    如今,咱們開始看一下用來移動畫刷經過圖形或者旋轉一個點雲的技術了。這些技術叫作動畫技術。可用的兩個動畫技術是異或繪圖和雙緩衝。

    讓咱們從異或繪圖開始。那咱們早前設置的窗體爲例,下邊的表達式將一個高亮的符號沿窗體對角線向下運動。

> (let ((width (send w :canvas-width))
        (height (send w :canvas-height))
        (mode (send w :draw-mode)))
    (send w :draw-mode 'xor)
    (dotimes (i (min width height))
             (send w :draw-symbol 'disk t i i)
             (send w :draw-symbol 'disk t i i))
    (send w :draw-mode mode))
NORMAL

    let語句裏的變量綁定肯定當前畫布的大小,而且保存當前的繪圖模式。代碼裏第一個表達式將繪圖模式設置爲xor(異或模式);該設置持續有效直到它運行到代碼底部重設爲止。剩餘語句是一個dotimes循環,該循環體包含兩個相同的繪圖表達式。第一條語句繪製這個符號,第二條語句再繪製一遍。由於繪圖系統處於xor模式,第二條繪圖命令的效果就是擦除了該符號。

    在一些系統裏,本例中的動畫可能運動得太快而很那見到,若是出現了這種狀況,你能夠向循環中的兩條繪圖命令之間插入一條pause表達式,好比(pause 2)。

    異或繪圖對於簡單對象經過一個簡單的背景時是頗有用的。然而,它確實有一些缺陷,隨着大量動畫技術的出現,這些缺陷下降了它的實用性:

  • 若是背景包含不少項目,當繪製的對象移動並經過背景裏的這些項目時,xor模式的倒置處理將會使正在繪製的對象變形。
  • XOR繪圖模式固有地引入了必定量的閃爍。在咱們的例子中,當符號處於屏幕之上的時候,它花費了更多的時間在擦除符號上。
  • 移動一些對象會致使必定量的變形,由於每次只能移動一個對象。
  • 若果背景顏色不是黑色或者白色的話,XOR繪圖模式裏的倒置背景像素的概念就不是一個良好定義的行爲。

不管如何,XOR繪圖仍然是一個有用的技術,甚至在彩色顯示器上,由於變成他本身的倒置這個屬性是保持原狀的。Lisp-Stat使用XOR模式繪圖的兩個目的:移動畫刷和繪製點狀標籤。

    第二個動畫方法是雙緩衝。在雙緩衝裏,一個影像不在屏幕上,而後它被拷貝到屏幕上。只要這個拷貝的過程足夠快,它將產生一個圖像到另外一個圖像的無閃爍轉換。爲了支持雙緩衝,繪圖系統提供了一個獨立的背景緩衝區,你能夠將該緩衝區想象爲在屏幕後邊的另外一個窗體,經過向窗體發送:start-buffering消息,你命令該窗體以前的全部的繪圖消息放到該緩衝區裏,:start-buffering方法將確保:當緩衝開始時,緩衝區的繪圖系統的狀態與窗體的狀態時相同的。當緩衝開始起做用的時候,傳遞給窗體的狀態改變指令對窗體和緩衝區都會有影響。繪圖操做隻影響緩衝區。當你結束在緩衝區中的繪圖時,你能夠將繪製的圖像傳遞給窗體,而且經過向窗體發送:buffer-to-screen消息來關閉緩衝。

    下標的表達式是經過雙緩衝的方法將一個符號沿窗體對角線向下移動的動畫:

> (let ((width (send w :canvas-width))
        (height (send w :canvas-height)))
    (dotimes (i (min width height))
             (send w :start-buffering)
             (send w :erase-window)
             (send w :draw-symbol 'disk t i i)
             (send w :buffer-to-screen)
             (pause 2)))
NIL
    須要擦除消息來清理緩衝區的前一個符號。使用雙緩衝,符號移動得更慢了,可是由於事實上沒有了閃爍,看起來移動得更平滑了。

    :buffer-to-screen消息也能夠接受一個矩形的座標做爲參數, 而後它只從緩衝區裏將指定的矩形區域拷貝到屏幕上。例如,若是你想改變圖形的內容而不改變座標軸,這個功能就頗有用了。

    若是你接連發送兩次:start-buffering消息,而在兩次發送之間沒有將緩衝區拷貝到屏幕上,影響就是增長了一個緩衝區計數,發送:buffer-to-screen消息將遞減該計數,僅噹噹計數達到0時纔將緩衝區拷貝到屏幕上。若是你依據另外一個方法(可能用到也可能沒用到這個緩衝區)編寫了一個方法的話,這個功能是頗有用的。例如,標準散點圖使用:redraw消息能夠用來重畫整個圖形,或者使用:redraw-content消息來重畫圖形的內容,這兩種重畫都要用到緩衝區,:redraw方法是依據:redraw-content消息和代碼來重畫座標。

    當你調試一個圖形算法的時候,你能夠將本身置於一個這樣的狀態,在那裏你不知道窗體是否是一個緩衝區,函數reset-graphics-buffer關閉全部緩衝區,同時將緩衝區計數設置爲0。發生錯誤或者中斷而返回到頂層也會重置緩衝區。

    每次只能有一個窗體使用該緩衝區。

練習 8.2

略。

8.4 響應事件

    到目前爲止,咱們能夠直接從解釋器控制咱們的圖形窗口。換句話說,第二章裏描述的圖形可以響應它們本身的鼠標動做,可以重畫和從新定義大小,等等。爲了容許窗體響應用戶產生的事件,當發生一個須要來自窗體的響應的事件時,繪圖系統將向每個窗體對象發送一個消息。

    經過從新改變大小或者露出窗體而產生的事件,明顯與特定窗體有關;換句話說,由鼠標或鍵盤動做產生的事件能夠根據一些不一樣的規則分配給各窗體。接受這些事件的窗體叫作焦點窗體,爲了肯定焦點窗體,不一樣的用戶界面遵循不停的慣例。有的接口假定焦點窗體就是包含光標的窗體,其它的則須要一個被強制設置爲當前窗體,例如經過點擊該窗體。Lisp-Stat將肯定焦點窗體的規則交給本地窗體系統。該窗體系統的接口負責將相應的用戶動做發送給合適的窗體對象。

8.4.1 重定義及露出窗體

    在窗體被用戶從新定義大小以後,系統首先要向窗體對象發送:resize消息,而後,再向窗體發送:redraw消息。窗體模糊的部分從新露出之後也會向窗體發送:redraw消息。經過響應這些消息,當窗體改變大小,其它窗體出現、消失和在它周圍移動時,該窗體均可以保持它的影像不變。

    舉個例子,讓咱們拿先前建立的試圖在其中心一個高亮的符號的那個窗體來講,咱們能夠經過只是用:redraw消息的方式來實現,可是爲了得到更多的實踐,讓咱們使用:redraw和:resize消息。首先,咱們能夠添加兩個槽來放置符號位置的座標:

> (send w :add-slot 'x (/ (send w :canvas-width) 2))
125
> (send w :add-slot 'y (/ (send w :canvas-height) 2))
125.5
而後定義這兩個槽的讀取方法:
> (defmeth w :x (&optional (val nil set))
    (if set (setf (slot-value 'x) val))
    (slot-value 'x))
:X
> (defmeth w :y (&optional (val nil set))
    (if set (setf (slot-value 'y) val))
    (slot-value 'y))
:Y
接下來,咱們能夠定義一個:resize消息,不管窗體什麼時候更新都會更新這些槽:
> (defmeth w :resize ()
    (send self :x (/ (send self :canvas-width) 2))
    (send self :y (/ (send self :canvas-height) 2)))
:RESIZE
最後,:redraw方法使用這些槽裏的信息繪製符號:
> (defmeth w :redraw ()
    (let ((x (round (send self :x)))
          (y (round (send self :y))))
      (send self :erase-window)
      (send self :draw-symbol 'disk t x y)))
:REDRAW
round函數是必須的,由於傳遞給:draw-symbol消息的參數必須是整型數據。

    這是全部標準繪圖須要使用的基本方法:當窗體改變大小時,窗體的大小基本信息將確認並保存,而後當須要的時候,重畫消息將得到此信息。

    當建立一個新的窗體的時候,首先發送一個:resize消息,而後發送一個:redraw消息。所以,在窗體第一次繪製以前,你能夠依賴於:resize調用。

練習 8.3

略。

8.4.2 鼠標事件

    鼠標事件有兩種類型:移動事件和點擊時間。當鼠標移動而且繪圖窗口是焦點窗口時,繪圖系統向窗體發送:do-motion消息,該參數帶兩個整型參數,鼠標新位置的x與y座標值。例如,咱們能夠定義一個:do-motion方法,該方法在鼠標移動的時候,使一個符號跟隨着鼠標位置的移動而移動。這樣的方法定義以下:

> (defmeth w :do-motion (x y)
    (send self :x x)
    (send self :y y)
    (send self :redraw))
:DO-MOTION
與每次鼠標移動一次就調整一次符號位置不一樣,僅僅響應鼠標點擊事件時才調整符號可能更好些,能夠經過首先移除:do-motion方法的方式來完成:
> (send w :delete-method :do-motion)
T
    而後,再定義一個:do-click方法。不管什麼時候發生鼠標點擊事件,系統都會向焦點窗體發送:do-click消息,它帶4個參數,前兩個參數是整型數,鼠標點擊位置的x與有座標,剩下的兩個參數是t或者nil,表示兩個修改符的狀態。第一個修改符是擴展修改符,第二個是選項修改符。用來發送這些修改符的方法依賴特殊的用戶接口。在Macintosh系統上它們分別對應的是按下shift鍵和option鍵。對於咱們的例子,響應一次點擊時間來調整符號位置的方法能夠這樣定義:
> (defmeth w :do-click (x y m1 m2)
    (send self :x x)
    (send self :y y)
    (send self :redraw))
:DO-CLICK
    咱們已經見過兩種將一個對象定位到窗體的方法,第一種是在鼠標移動時跟蹤鼠標,第二種是將對象移動到當鼠標點擊的位置。不少狀況下,經過容許鼠標拖拽時持續地移動對象,將這兩種方法組合使用是頗有用的(好比按下鼠標時進行移動)。爲了容許鼠標按下時執行一個動做,你能夠在:do-click消息體裏向窗體對象發送:while-button-down消息,該消息須要一個參數,是一個函數,同時它也接受一個額外的可選參數。若是該可選參數是t(t是其默認值),那麼當鼠標按下時,鼠標每移動一下函數就被調用一次。若是該可選參數是nil,函數將持續調用,直到鼠標釋放爲止。該函數參數應該有兩個參數,鼠標當前位置的x與y座標。當鼠標釋放的時候,:while-button-down消息對應的方法返回。

    爲了使拖拽在咱們的例子裏具體化,咱們能夠從新定義:do-click方法:

> (defmeth w :do-click (x y m1 m2)
  (flet ((set-symbol (x y)
                     (send self :x x)
                     (send self :y y)
                     (send self :redraw)))
    (set-symbol x y) (send self :while-button-down #'set-symbol))) :DO-CLICK
對局部函數set-symbol的首次調用將保證首次點擊時將符號移動到鼠標點擊的位置。

    由於在這個定義裏:while-button-down消息使用不帶可選參數的形式,set-symbol函數僅當鼠標按下時移動纔會調用。若是最後一個表達式替換成小標的形式,那麼set-symbol函數甚至在鼠標不動的時候都會被調用。

(send self :while-button-down #'set-symbol nil)))
在不少系統上,當鼠標按下時,這將致使符號的快速閃爍。持續調用一個函數的能力對於實現一個按鍵操做是頗有用的,該按鍵操做在按下時會引發一個動做發生。Lisp-Stat裏的旋轉圖形的旋轉控制按鈕就是這麼實現的。

    與拖拽一個完整的對象不一樣,有時最好是拖拽一個對象的外接矩形。:drag-grey-rect消息使用XOR繪圖模式在窗體上拖拽一個矩形,該消息須要四個參數:鼠標當前位置的x與y座標,矩形的寬度與長度。它將繪製一個虛線矩形,而後對鼠標動做作出反應,直到鼠標釋放。當鼠標釋放的時候,該方法從屏幕上移除該虛線矩形,並返回最終矩形座標的列表。爲了在咱們的例子裏使用這個消息,咱們能夠這樣定義:do-click方法:

> (defmeth w :do-click (x y m1 m2)
    (let ((xy (send self :dray-grey-rect x y 5 5)))
      (send self :x (+ 3 (first xy)))
      (send self :y (+ 3 (second xy)))
      (send self :redraw)))
:DO-CLICK
該定義使用一個寬度爲5個像素的正方形,在符號所在的矩形內每一個座標中心都加上3.

    默認地,:drag-grey-rect方法將鼠標光標放置在矩形的右下側,你能夠提供兩個表示偏移量(即矩形右邊和下邊相對鼠標位置的像素數值)的額外的整型參數給方法。所以下式將光標置於矩形的中心。

(send self :drag-grey-rect x y 5 5 3 3)
    咱們能夠多增長一個變量到咱們的例子中,重定義:do-click方法以下:
> (defmeth w :do-click (x y m1 m2)
    (let ((cursor (send self :cursor)))
      (send self :cursor 'finger)
      (let ((xy (send self :drag-grey-rect x y 5 5)))
        (send self :x (+ 3 (first xy)))
        (send self :y (+ 3 (second xy)))
        (send self :redraw))
      (send self :cursor cursor)))
:DO-CLICK

當符號拖拽的時候,這致使光標變成一個有一根手指頭的手型。可用的光標列表能夠經過:cursor-symbols函數來返回,它應該包含arrow, brush, hand和finger。每一個圖形窗體都會保留一個使用的光標,不管什麼時候,只要窗體焦點窗體同時鼠標在窗口之上。在一些簡單的例子裏,不須要改變光標。可是若是一幅圖形在不一樣的繪圖模式裏,那麼給用戶一個光宇當前模式的可視化暗示就很重要了。全部標準的Lisp-Stat圖形均可以存在於兩種模式之中,刷模式和選擇模式。另外,第九章裏的一些例子將告訴你如何向圖形裏添加新的模式。多個可用模式很容易讓人費解,爲每一個模式使用不一樣的光標能夠減小這種費解。

練習 8.4

略。

連寫 8.5

略。

8.4.3 鍵盤事件

    當敲擊一個鍵,而且當前繪圖窗體是焦點窗體的時候,那麼將向該窗體發送:do-key消息,該消息帶3個參數。第一個參數是對應敲擊的鍵值的Lisp字符,剩下的兩個參數是調節器,用來表示當鍵被敲擊的時候,是否有shift、option或control等鍵被按下。

    對於咱們的例子,咱們能夠定義一個:do-key方法,該方法將符號向上、下、右或左移動,分別對應的按下u, d, r,或者l按鍵。與預先設定的固定步進長度不一樣,最好是增長一個槽和一個對應的讀取方法:

> (send w :add-slot 'step-size 10)
10

> (defmeth w :step-size (&optional (val nil set))
    (if set (setf (slot-value 'step-size) val))
    (slot-value 'step-size))
:STEP-SIZE
經過指定一個量來移動符號的方法,能夠這樣定義:
> (defmeth w :move (x y)
    (send self :x (+ x (send self :x)))
    (send self :y (+ y (send self :y)))
    (send self :redraw))
:MOVE
>
使用這兩個方法,:do-key方法能夠這樣定義:
> (defmeth w :do-key (c m1 m2)
    (let ((step (send self :step-size)))
      (case c
        (#\u (send self :move 0 (- step)))
        (#\d (send self :move 0 step))
        (#\r (send self :move step 0))
        (#\l (send self :move (- step) 0)))))
:DO-KEY
經過將case語句裏的#\u這樣的鍵形式替換成(#\u #\U)列表,咱們能夠同時使用大小寫字符。

8.4.4 空閒操做

    經過按下帶擴展調節器的控制鍵,Lisp-Stat選擇圖形操做能夠指示圖形持續旋轉。這能夠經過針對這些圖形對象來指示系統使能ldling來實現。當ldling對於繪圖窗體爲使能狀態時,系統每次從其時間循環裏發送一次:do-idle消息給繪圖窗體。

    :idle-on消息用來肯定idling是否使能並使能之。不帶參數的狀況下,若是idling是使能的它將返回t,未使能則返回nil;在帶一個參數的狀況下,若是參數爲ture則使能idling狀態,若是是nil則關閉使能。爲了不idle功能失去控制,若是在idle動做裏發生一個錯誤,idling將自動取消使能。

    爲了繼續咱們的例子,咱們可使用:do-idle方法使重提裏的符號隨機移動,每次調用使選擇向各個方向移動的可能性都是1/4。使用爲:step-size和:move定義的方法,:do-idle方法能夠這樣編寫:

> (defmeth w :do-idle ()
    (let ((step (send self :step-size)))
      (case (random 4)
        (0 (send self :move 0 (- step)))
        (1 (send self :move 0 step))
        (2 (send self :move step 0))
        (3 (send self :move (- step) 0)))))
:DO-IDLE

random函數帶一個整型數據n爲參數,返回一個隨機均勻的從0到n-1的整數中的一個。如下表達式是開始隨機遍歷和中止遍歷:

> (send w :idle-on t)
T

> (send w :idle-on nil)
NIL
練習 8.6

略。

練習 8.7

略。

8.4.5 菜單

    每一個繪圖窗體都提供一個支持想本身加入一個菜單的支持。引發菜單表示的特定的行爲以來特定的窗體系統和用戶接口。在XLISP-STAT的Macintosh版本里,當窗體是激活狀態時,繪圖窗體的菜單是安裝在菜單欄的。在X11版本里,經過在窗體頂端按下一個菜單按鈕來使菜單彈出。

    爲了在一個繪圖窗體里加入一個菜單,你能夠向該窗體對象發送:menu消息,它的菜單式就是菜單自身;參數爲nil將會使菜單從當前窗體移除;若是不帶參數,該消息將返回窗體的當前菜單。

    對於咱們的窗體w,這個實現了一個簡單的隨機遍歷模擬器的窗體,能有一個菜單使遍歷從窗體中心開始,並能容許打開和關閉遍歷功能,這多是頗有用的。重啓窗體的方法能夠這樣定義:

> (defmeth w :restart ()
    (send self :x (/ (send self :canvas-width) 2))
    (send self :y (/ (send self :canvas-height) 2))
    (send self :redraw))
:RESTART
發送消息的菜單對象能夠這樣給定:
> (setf restart-item
        (send menu-item-proto :new "Restart"
              :action #'(lambda () (send w :restart))))
#<Object: 13980f0, prototype = MENU-ITEM-PROTO, title = "Restart">
開始和中止遍歷的菜單項這樣給定:
> (setf run-item
        (send menu-item-proto :new "Run"
              :action
              #'(lambda ()
                  (send w :idle-on (not (send w :idle-on))))))
#<Object: 1396f50, prototype = MENU-ITEM-PROTO, title = "Run">
這個菜單項控制空閒的開與關。更新方法保證當遍歷打開時,芥菜單項包含一個複選記號:
> (defmeth run-item :update ()
    (send self :mark (send w :idle-on)))
:UPDATE
最後,下邊的表達式構造並安裝菜單:
> (setf menu (send menu-proto :new "Random Walk"))
#<Object: 13a493c, prototype = MENU-PROTO, title = "Random Walk">
> (send menu :append-items restart-item run-item)
NIL
> (send w :menu menu)
#<Object: 13a493c, prototype = MENU-PROTO, title = "Random Walk">
    經過向菜單發送:popup消息,也可能響應一個鼠標事件來彈出一個菜單。這個消息的方法須要兩個參數,表示菜單顯示的點位的座標值。默認地,這些座標被解釋成是相對於屏幕原點的,可是若是使用了額外的可選參數,那麼該座標被認爲是相對於窗體內容的左上角座標的。所以,下式定義了一個:do-click方法,在鼠標點擊的位置將彈出窗體菜單。

8.5 額外的特性

8.5.1 畫布維度

    窗體的畫布是一個矩形區域,其座標系統的原點在其左上角。畫布的寬度與高度可使固定的也能夠是可變化的。若是是可變化的,它的寬度與高度與窗體內容的寬度與高度是相等的。對於一個新窗體,這個是默認的。爲了給定窗體一個固定的寬度,你能夠向窗體發送一個:has-h-scroll消息,其參數要麼是t要麼是一個正整數。整數用作畫布的固定寬度,而且安裝一個滾動條。固定畫布的高度使用:has-v-scroll消息。能夠給定:has-h-scroll和:has-v-scroll消息nil做爲參數以使寬度和高度可變。調用不帶參數的消息,若是當前的維度是固定的它們返回t,若是它是可變的就返回nil。對於繪圖窗體原型,畫布的寬度和高度的值能夠這樣指定,向該原型的:isnew方法傳遞:has-h-scroll和:has-v-scroll關鍵字。

    當一個維度是固定的時候,圖形窗體爲那個維度包含一個滾動條,這些滾動條能夠用來定位畫布裏的窗體。你也能夠經過向窗體對象發送帶兩個參數的:scroll消息來定位窗體,窗體的左上角座標在畫布座標系內。:scroll消息也能夠以不帶參數的形式發送,以左上角當前座標的列表。當前窗體矩形的座標可使用:view-rect消息獲取。

    在大多數用戶接口裏滾動條都容許兩種形式的滾動,一次滾動一行和一次滾動一頁。默認地,行滾動每次滾動一個像素,頁滾動每次滾動5個像素。你可使用:h-scroll-incs和:v-scroll-incs消息來改變這些值。不帶參數和帶兩個參數,行和頁都遞增。不帶參數則返回一個當前增量的列表。

    固定畫對於在一個小屏幕上檢測一個散點圖或者散點圖矩陣是頗有用的。它們不會用於標準圖形的默認設置裏,除非是在name-list圖形裏。可是全部的圖形都容許附加的滾動條,它是經過圖形菜單的Option...對話框設置的。

8.5.2 剪切

    有時候,可以將繪圖約束到窗體的一個子矩形裏,而不須要直接地強制性地檢測每個繪圖動做的參數。這能夠經過設置一個剪切矩形來完成。:clip-rect消息是用來設置這樣的矩形的,它的參數多是矩形的四個座標,或者是nil值以關閉剪切功能。它返回當前剪切矩形左邊的列表或者返回nil。也能夠不使用任何座標來調用該消息,目的是返回當前的剪切狀態而不改變剪切消息。

    若是設置了剪切矩形,當窗體大小改變時,該剪切矩形不會自動改變大小,你不得不在你本身的:resize方法裏調整它。

8.5.3 保存圖形

    Lisp-Stat實現裏應該提供一個將圖形裏的圖像保存成文件的方法。在Macinton系統的XLISP-STAT版本里,Edit菜單裏的Copy命令式可用的,它向窗體發送:copy-to-clip消息,目的是將圖形拷貝到剪切板。在SunView系統和X11系統的版本里,能夠向窗體對象發送:save-image消息,該消息的方法可使用繪圖窗體的:redraw方法來構造要保存的圖像,若是:redraw方法不能重構當前圖像的話它就不能合理地工做。

8.5.4 增長新的顏色

    Lisp-Stat系統可用的顏色可經過符號來識別,好比符號red和green。典型的顏色系統至少支持black, white, red, green, blues, cyan, magenta和yellow。你也可使用color-symbols函數來獲取可用顏色的列表。你也能夠經過使用make-color函數要求系統生成一個新的可用的顏色,該函數帶四個參數,爲新顏色命名的符號和在[0, 1]範圍內的三個實數,用來表示在顏色的RGB表示法裏紅綠藍三色的貢獻率。

    在今天的一些顏色工做站裏,顏色是一個稀缺資源。在不一樣時間能夠顯示的不一樣顏色的數量是十分巨大的,大概有幾百萬中,可是能夠在同一時刻顯示的顏色數量通常僅有256種,也許甚至只有16種。結果,系統可能沒法分配給你一個你請求色顏色。在這種狀況下可能會發生一個錯誤信號,或者系統肯恩分配一個這樣的顏色,分配給你一個和你須要的顏色極其相近的顏色。

    當你再也不須要某個你使用make-color函數分配的顏色的時候,你可使用free-color函數釋放它。該函數只帶一個參數,你要釋放的那個顏色的符號。試圖使用一個已經釋放的顏色會引起一個錯誤。你可使用make-color重定義一個顏色而不須要釋放前邊定義的顏色。

8.5.5 增長新光標

    在Lisp-Stat裏,你能夠向提供的實現裏添加新的光標。make-cursor函數須要兩個必需參數和三個可選參數。第一個參數是一個用來命名該光標的符號。接下來的兩個參數是兩個位圖,它們是由0和1組成的二維數組,它們大小相等。第一個位圖是光標的影像;第二個是光標的遮罩。影像裏的每個1都是用前景色繪製的,一般是黑色,每個0都用背景色繪製,一般是白色,提供遮罩位圖裏有的響應的元素。遮罩裏爲0的像素不受映像。若是沒有使用遮罩,遮罩層能夠當作是全1的。最後兩個可選參數是整型數,描述光標的熱點的座標,即圖像裏與鼠標位置相關聯的點。默認的熱點是左上角。

    今天大多數使用中的工做站都使用由16X16的位圖構造的光標。有的工做站可能容許其它尺寸的光標。best-cursor-size函數返回了系統首選圖標的寬度和高度的列表。該函數也能夠只傳遞兩個整型參數,而後返回與這兩個參數表示的光標的寬度和高度最接近的一個光標。系統可能對不是最優尺寸的光標進行裁剪或擴張。

    cursor-symbols函數返回可用光標的列表。free-cursor接受一個光標符號爲參數而後釋放與之相關聯的光標。試圖使用一個已經釋放的光標會引起一個錯誤。你也能夠經過不釋放前一個定義的光標,使用make-cursor函數來重定義這個光標。

8.6 一個例子

    做爲一個例子,針對一個簡單的位圖構造器,咱們可使用繪圖窗體原型做爲基礎。該想法是經過黑色和白色方塊或者矩形來表示位圖,黑色表明1,白色表明0。在方塊裏點擊鼠標將該表位圖數組裏對應的實體,從0改爲1,或者從1改爲0,而後更新屏幕。

    爲了開始之,讓咱們定義一個原型,該原型包含一個針對位圖的槽,還包含兩個處理顯示在窗體的矩形網格的橫軸和縱軸座標的兩個槽。

> (defproto bitmap-edit-proto
    '(bitmap h v) nil graph-window-proto)
BITMAP-EDIT-PROTO
:is-new方法應該須要兩個整型參數,位圖的寬度與高度:
> (defmeth bitmap-edit-proto :isnew (width height)
    (call-next-method)
    (setf (slot-value 'bitmap)
          (make-array (list height width) :initial-element 0)))
:ISNEW
咱們還須要這三個槽的讀取函數,它們這樣定義:
> (defmeth bitmap-edit-proto :bitmap () (slot-value 'bitmap))
:BITMAP
> (defmeth bitmap-edit-proto :v () (slot-value 'v))
:V
> (defmeth bitmap-edit-proto :h () (slot-value 'h))
:H
矩形座標能夠設置到:resize方法裏:
> (defmeth bitmap-edit-proto :resize ()
    (let ((m (array-dimension (send self :bitmap) 0))
          (n (array-dimension (send self :bitmap) 1))
          (height (send self :canvas-height))
          (width (send self :canvas-width)))
      (setf (slot-value 'v)
            (coerce (floor (* (iseq 0 m) (/ height m)))
                    'vector))
      (setf (slot-value 'h)
            (coerce (floor (* (iseq 0 n) (/ width n)))
                    'vector))))
:RESIZE
座標保存爲矢量格式,目的是能夠快速隨機獲取它們的元素。

    爲了繪製一個特定的位圖矩形,一個方法給定以下:

> (defmeth bitmap-edit-proto :draw-pixel (i j)
    (let* ((b (send self :bitmap))
           (v (send self :v))
           (h (send self :h))
           (left (aref h j))
           (right (aref h (+ j 1)))
           (top (aref v i))
           (bottom (aref v (+ i 1))))
      (send self (if (= 1 (aref b i j)) :paint-rect :erase-rect)
            (left top (- right left) (- bottom top)))))
:DRAW-PIXEL
方法裏的if表達式選擇合適的消息選擇器符號來使用,:paint-rect用來繪製該像素,:erase-rect用來擦除它。:redraw方法能夠僅擦除窗體,而後爲位圖數組的每一個元素髮送一次:draw-pixel消息:
> (defmeth bitmap-edit-proto :redraw ()
    (let* ((b (send self :bitmap))
           (m (array-dimension b 0))
           (n (array-dimension b 1))
           (width (send self :canvas-width))
           (height (send self :canvas-height)))
      (send self :start-buffering)
      (send self :erase-rect 0 0 width height)
      (dotimes (i m)
               (dotimes (j n)
                        (send self :draw-pixel i j)))
      (send self :buffer-to-screen)))
:REDRAW
    爲了支持鼠標點擊方法,咱們能夠定義一個這樣的方法:它帶一個座標對爲參數,肯定對應像素的位置,在位圖數組裏轉置該值,然偶重畫它的圖像:
> (defmeth bitmap-edit-proto :set-pixel (x y)
    (let* ((b (send self :bitmap))
           (m (array-dimension b 0))
           (n (array-dimension b 1))
           (width (send self :canvas-width))
           (height (send self :canvas-height))
           (i (min (floor (* y (/ m height))) (- m 1)))
           (j (min (floor (* x (/ n width))) (- n 1))))
      (setf (aref b i j) (if (= (aref b i j) 1) 0 1))
      (send self :draw-piel i j)))
:SET-PIXEL

圖8.4 一個簡單的位圖編輯器

而後,:do-click方法簡化爲:

> (defmeth bitmap-edit-proto :do-click (x y m1 m2)
    (send self :set-pixel x y))
:DO-CLICK
    如今咱們能夠爲一個 16X16的位圖構造一個位圖編輯器:
> (setf w (send bitmap-edit-proto :new 16 16))

在一些鼠標點擊結果可能好圖8.4很像。

    這個例子上的多數變量是可能的,爲了將位圖分配給一個全局變量,咱們可能須要將配位圖而增長一個方法:
(defmeth bitmap-edit-proto :name-bitmap ()
    (let ((str (get-string-dialog "Symbol for the bitmap;")))
      (if str 
          (let ((name (with-input-from-string (s str) (read s))))
            (setf (symbol-value name) (send self :bitmap))))))
:NAME-BITMAP

若是OK按鈕被點擊那麼get-string-dialog返回一個字符串,而後with-input-from-strin容許read函數使用標準讀取慣例,來解壓第一個實體。尤爲地,該操做在構造符號以前,將名字轉化爲大寫字母。

    另外一個有用的方法多是這樣的:安裝當前位圖做爲窗體光標。

> (defmeth bitmap-edit-proto :bitmap-as-cursor (yes)
    (if yes (make-cursor 'temp-cursor (send self :bitmap)))
    (send self :cusor (if yse 'temp-cursor 'arrow)))
:BITMAP-AS-CURSOR
若是咱們經過提供一個有菜單的進行讀取的,全部這些方法將很容易使用。咱們能夠爲咱們的窗體定義一個菜單:
> (setf bitmenu (send menu-proto :new "Bitmap"))
#<Object: 14ff1f4, prototype = MENU-PROTO, title = "Bitmap">

> (setf name-item 
        (send menu-item-proto :new "Name Bitmap..."
              :action #'(lambda () (send w :name-bitmap))))
還有:
> (setf cursor-item
        (send menu-item-proto :new "Use as Cursor"
              :action
              #'(lambda ()
                  (let ((mark (send cursor-item :mark)))
                    (send w :bitmap-as-cursor (not mark))
                    (send cursor-item :mark (not mark))))))
#<Object: 150a8e0, prototype = MENU-ITEM-PROTO, title = "Use as Cursor">

cursor-item的動做函數action使用項的複選標記,目的是跟蹤位圖或箭頭當前狀態下是否正在使用,其表達式是:

> (send bitmenu :append-items name-item cursor-item)
(send w :menu bitmenu)

它們將把菜單項安裝到菜單裏,將菜單安裝到窗體裏。

練習 8.8
略。

練習 8.9 略。

相關文章
相關標籤/搜索