分享

VFP 的Grid表格透析

 Alkaid2015 2012-07-19
評論:Grid是VFP很重要的一個控制 項.可不要把VB的Grid來看待喔!
否則你會用的很沮喪.不管新手或老手.我都建議他.有空多吸收此文章.

VFP 的Grid表格透析

RMH 於 2001-12-17 12:41:05 發表:
作者 Vlad Grynchyshyn
譯者 RMH

VFP 中的表格 第一部分

什麼是表格和什麼時候使用它?

表格控制項是一組允許在一個表格一樣的可捲動列表中顯示資料的 VFP 物件. 表格由表格物件自己和一組列組成. 各列必須有一個 header 物件和在表格列中顯示資料的控制項. 表格列中的控制項用於顯示和編輯資料. 表格是一個帶格線條的矩形, 和頂部的標題, 捲動條和一些其他有用的東西, 如記錄標記, 刪除標記, 分隔條等.

說實話, 表格顯示資料不象 Excel sheet 那樣自由. 表格要求用 VFP 記錄源 (別名) 來顯示一些東西. 要在同一列的不同行中顯示不同類型的資料是困難的. 表格中的所有行的高度都是相同的, 並且表格中一列中的所有行的寬度也是相同的. 當然還有一些其他相當奇怪的和不可思議的限制, 除非我們記住表格是一個真實的基於早期版本的 FoxPro 的流覽視窗來編寫的控制項. 這一事實回答了許多關於表格的奇怪的東西和行為的 &為什麼&. 儘管有許多的麻煩, 表格還是相當有用的控制項並在受到 VFP 程式師的歡迎, 因此也有許多的處理和方案來打破表格的限制並用它來產生更好的效果. 局限或奇怪的東西將不再是決定是否使用表格的關鍵.

表格控制項對於在一個簡潔的表單中顯示(流覽)資料是有用的 (每頁顯示大量資料). 表格對於搜索和定位資料比彈式功能表或整頁的下拉清單好. 持久穩固的使用表格作為定位資料不是一個好的主意, 因為表格通常佔用許多空間. 許多應用程式有一個表格作為一個主要的控制表單 - 複雜的帶有許多功能的可顯示子表單來進行資料編輯的表格.

表格對於所有類型的規則的唯讀資料是有用的. 使用表格進行資料編輯不是一個好主意. 在表格中進行資料編輯的好處僅僅是易於組織和管理應用程式. 在表格中進行資料編輯時如果要打破 VFP 表格的限制會有許多問題. 但是, 例如發票或訂單表單的行項, 使用表格來進行編輯是更好的, 用戶也會感到更舒服. 這樣的表單是一個例外; 不要僅僅因為表格對於編輯資料時的簡單就把它放在每一個表單中. 當你這樣做時, 你會很快發現自己處於麻煩之中, 因為表格是如此複雜且易於失去控制的控制項. 這將要求你付出更多的努力來指出問題, 找出解決方案和修正更多的問題. 通常, 資料編輯使用文本框顯示欄位和帶有定位,保存/恢復和一些其他特定按鈕工具欄來移動記錄的的表單. 為表格生成一個這樣的表單是花費大量時間的一種方法. 當然, 如果你有時間並喜歡玩耍怪異的東西, 表格是一種消磨時光並找出方案和處理辦法的最好的控制項.


表格自動進行列的重綁定

表格中最常出現的奇怪的東西是列的 control sources 的自動改變. 你會發現突然表格列顯示了其他的不是你在列的 ControlSource 屬性中指定的資料. 另外, 列中顯示的資料的順序不是你重新安排過的順序. 為什麼會這樣?

======================================================
因為表格的 RecordSource 屬性在設計時被改變. 在表格的 RecordSource 屬性被改變後所有的 ControlSource 值被清除了. 在此情況下製作一個這些值的備份, 通常是在列的 Comment 屬性中.

因為表格的 RecordSource 屬性在運行時被改變. 好了, 如果你要這樣做, 保存所有的 control sources, 然後恢復它們.

可能的原因是表格被重建了. 關於這一點將在下一章中說明.
======================================================

該行為的副作用並不嚴重. 通常顯示的資料沒有改變, 除非表格有一些複雜的功能和結構體系. 例如, 列值顯示的是運算式 - 運算式將丟失並只顯示欄位. 對於用戶來說在表格列中顯示 ID 欄位 (主關鍵字段)也不是一件好事. 最危險的是當你在表格中有一些不同的控制項時. 當列中有一個核取方塊控制項時, 但表格決定使用字元型欄位作為該列的 control source 時, VFP 將顯示一個關於資料類型失配的錯誤: &控制項不支援該資料類型&.

無論是上述何種情況, 當以任何方式改變 record source 時, 都有消除表格重建的方案.

以下示例代碼用列的 Comment 屬性保存 control sources 並恢復它們.

&& 備份各列的 ControlSource
with {grid}
local nColumnIndex
for m.nColumnIndex = 1 to .ColumnCount
.Columns(m.nColumnIndex).Comment = .Columns(m.nColumnIndex).ControlSource
endfor
endwith

&& 恢復各列的 ControlSource
with {grid}
local nColumnIndex
for m.nColumnIndex = 1 to .ColumnCount
if !empty(.Columns(m.nColumnIndex).Comment)
.Columns(m.nColumnIndex).ControlSource = .Columns(m.nColumnIndex).Comment
endif
endfor
endwith

上面代碼中的 {grid} 是表格物件的引用(如:Thisform.Grid 或
Thisform.Pageframe1.Page1.Grid1).

把這些代碼放到表格類的一個方法中是一個不錯的主意. 這在設計時因改變 record source 而造成 ControlSource 值丟失時在表格類的 Init 方法中調它來恢復 control sources 也是一種好辦法.

重大注意: 在完全恢復 control sources 前不要執行任何表單上的可視控制項或表格的 refreshing. 否則如果你在表格列中使用了自定義控制項時你將會遇到一個錯誤資訊 &控制項不支援該資料類型&. 這是因為正如前面提及的列中可能使用的打亂了的 control sources 中的不正確的欄位類型造成的.

表格自動重構

你是否發現過你的表格的行為不再象你在設計時設計的那樣的情況? 列中的自定義控制項丟失? 列, 標題或控制項事件中的代碼不再運行? 表格的重構行為會完全移去所有的表格控制項和列並用默認的 VFP 控制項和屬性設置再次重建表格. 這造成所有列物件中的所有方法, 屬性設置和物件丟失. 列的 CurrentControl 屬性重置為默認的文本框控制項. 自定義標題丟失. 通常這是一種不知道表格發生了什麼情況的災難. 下面說明種種原因及解決辦法.

1. 表格自己重構總是發生在 RecordSource 別名被關閉後. 如果這是一個視圖, 通常在你 requery 視圖時重構不會發生. 如果它是一個 SQL 語句, 當你指定另一個 SQL 語句或關閉表格使用的用於保存查詢結果的別名時重構就會發生. 當你使用 SQL Pass-Through 來再次查詢資料到表格的 record source 使用的別名時重構也會發生, 即使你任然使用的是視圖.

要避免在刷新表格的 record source 的重構, 你需要在上述的任何表格 record source 刷新前, 指定一個空串 (不是一個空格 - " ", 而是空串 - "") 到 Record source. 檢查你的代碼是否正確, 你是否以正確的順序這樣做了, 或其他東西沒有打亂正確的 refreshing 處理順序. (你可以在表格的 BeforeRowColChange 事件中放入 SET STEP ON 或設置跟蹤中斷點來跟蹤代碼). 在 record source 刷新後, 再次指定 record source 到表格. 在此情況下重構不會發生, 但是, 因為指定 record source , 列會發生自動重綁定. 以下是如何用一小點代碼來修正這種情況的示例.

* 下麵保存 control sources
...........
{grid}.RecordSource = ""
* 執行 record source 的刷新
...........
* 恢復 record source
{grid}.RecordSource = "{RecordSourceName}"
* 下麵恢復列的 control sources
...........

在上面代碼中 {grid} 是表格物件的引用, {RecordSourceName} 是用於 record source 的別名或 SQL 語句. 通常情況下使用 SQL 語句作為 record source 別名會使列的 ControlSource 改變, 因為 VFP 總是改變它來表示為 &Alias.FieldName&格式而不管在設計時只指定了欄位名. 以上方法在改變表格的 SQL 語句時也是有用的.

重要注意: 在語句 &RecordSource=""& 後到完全恢復 control source 之間, 不要執行任何表單上的可視控制項或表格的 refreshing. 否則如果你在表格列中使用了自定義控制項, 你將可能收到一個 &控制項不支援該資料類型& 錯誤.

另一種避免表格重構的方法是使用表格的 BeforeRowColChange 事件. BeforeRowColChange 事件在每次表格將要重構時被激發. 它在包括表格別名關閉時, SQL Pass-Through 游標重獲取等時發生. 無論表格是否可見, 具有焦點及表格的配置. 最令人驚奇的是放置 NODEFAULT 到該事件中以使資料改變時避免表格重構. 但是, 在此情況下表格會顯示不可思議的錯誤行為.

thisform.GridRefreshing = .T. && 告訴所有表格控制項
&& 將重新獲取表格資料
... 執行資料查詢
thisform.Grid.RecordSource = thisform.Grid.RecordSource
thisform.Refresh && 或表格刷新
DOEVENTS && 如果需要 - 只測試不要該命令
&& 在此時表格停止自己重構>
thisform.GridRefreshing = .F.

在表格類的 BeforeRowColChange 事件中放入以下代碼:

if PEMStatus(thisform,"GridRefreshing",5) AND thisform.GridRefreshing
nodefault
return
endif

如果你放置上面代碼到表格類中這樣該功能一般性的, 包括使 GridRefreshing 屬性作為你的表格類的屬性. 有時要求設置焦點到表格外然後再設置焦點回到表格, 因為表格中的當前單元會在使用該方法來避免表格重構時顯示星號 (&*******&).

不幸的是, 沒有辦法知道 BeforeRowColChange 事件被調用的原因是因為它的重構還是移動表格單格的焦點或是表格獲得焦點時. 就象在示例中一樣使用一個標記. 如果你有時間, 你也可以生成一個使用透明的形狀控制項複蓋在表格上的表格類來捕捉所有滑鼠事件. 採用該方法表格將可以知道表格的 BeforeRowColChange 激發的原因. 該方法也可以捕捉 KeyPress 事件 (最好是設置表單的 KeyPreview=.T.). 最後, 表格應該放到容器中來捕捉表格得到焦點時刻 (BeforeRowColChange 也將在表格得到焦點時激發).

兩種方法都有一個重大的缺點: 要求在所有造成重構的地方放置代碼. 當這樣的地方位於多個表單和類中時, 如, 用 SQL Pass-Through 功能重新查詢一些別名的時, 要定位所有的這些地方是困難的並且它們要求一個表格的引用來避免重構. 它有時也會造成不希望的列的自動再綁定. 表格常用於顯示視圖中的動態資料, 因此它在 requiry 時會被其他的資料刷新. 表格的重構在視圖 requiry 時不會發生. 但是, 當移動(升級)應用程式到使用遠端視圖時程式師常常決定使用 SQL Pass Through 功能來處理資料. 在這樣的情況下各個被 SQL Pass-Through 使用的別名的 requery 將造成重構. 因此, 程式師在這裏的主要錯誤也只是 requery 視圖並象以前一樣放入代碼到這裏. 它是一條單一的命令, 因此程式師常常在許多地方放入這樣的命令而沒有注意到這樣做的不正確之處. 在升級到使用 SQL Pass-Through 功能時所有 requery 語句應該用適當的命令替換. 另外, 在發現重構行為時, 程式師開始找所有該別名 requery 的地方. 它可能交叉地存在于表單和類中的多個地方, 這就成為一個大的問題. 訣竅: 放置資料 requery, 關閉, 打開和重打開 (以及其他所有與資料在關的動作) 在一個地方 - 類的方法或函數中, 並用該類的物件引用來調用適當的功能. 總是假設所有資料功能會在今後的一些附加的代碼中進行 requery, 即使當它只是一個簡單的視圖的 requery. 使用這種方法將有助於你在你需要修改某些東西而查找所有資料動作的地方時節約時間. 表格重構是當其出現時你不能避免的這種情況之一. 例如, 創建一個具有所有必需的, 處理它顯示的資料的方法的表格類. 然後使用表格類的物件引用. 這樣在所有地方的資料刷新請求將成為一個單一的功能調用. 好了, 也許你很少創建一個有如此需求的應用程式…

因表格重構行為的失敗, 程式師有時創建並維護表格使用的 Record Source 的游標, 然後刷新用刪除和複製來刷新這樣的游標. 在你已經有了這樣的游標的情況下, 從游標中刪除所有的資料並再次添加它們是不困難的.

2. 重構發生在表格初始化且 record source 屬性為空或 record source 不存在時 (別名沒有打開). 在此情況下表格自己重構並使用當前存在的別名作為 record source (或者在當前工作區中沒有打開表時保持為空, 但所有的列都不存在了). 如果你需要打開 record source 在一些其他的事件中(不是表單的 Load 方法中, 在表格初始化前), 使用下面的技術.

在表單的 Load 事件中創建一個空的與表格使用的 record source 結構相同的游標; 表格的 record source 屬性將使用該空的游標別名. 然後, 當你打開真正的資料時, 指定一個空的串到表格的 record source, 打開資料然後再次指定真正的資料別名到表格的 record source. 別一種處理方法是放置一個不可見的在它的 INIT 事件中創建空的游標的自定義控制項. 但是, 要保證該控制項的 Init 事件在表格的 INIT 之前激發, 否則將會發生重構. 第二種方法是在運行時添加表格到表單. 創建一個表格類並不在設計時把它放入表單. 在 Init 事件代碼中放入一個 AddObject 方法調用來在要使用的別名準備好後添加表格到表單.

3. 表格的自己重構在列數改變為零或 -1 發生. 我希望你不要這樣做(指設置 ColumnCount 為 0 或 -1), 你這樣做了嗎? Wink 總之, 它可以用於簡單的可以打開任何表並在表格中流覽它們的管理性表單. 但是, 由於重構的原因, 這樣的表格具有很大的功能限制, 或者所有的表格功能將放入到類中並在運行時的重構後添加到表格.

4. 表格的自己重構將在 record source 超出範圍時發生. 這通常發生在當 record source 指定在一個資料工作期, 但表格確初始化在另一個資料工作期中時, 這樣當它試著刷新自己時, 被另一個資料工作期使用的 record source 對於當前資料工作期來說是不存在的. 另一種情況是當程式師使用大量的資料工作期並在其間切換時發生.

重構不能以用附加的表格列的引用來避免它 - 列與表格是分離的. 另外, 讓表格使用已經在類中定義的列也不能避免重構 - 這是當在表單上使用容器類時容器類的子物件不能被刪除這一規則的例外.

別一種流行的消除表格重構問題的方法是動態地創建表格. 用你的所有列定義代碼創建一個自定義表格類. 當 requery 資料時, 從表單中移去表格控制項, requery 資料, 然後在運行時再次添加表格到表單並設置它的位置. 這要求首先處理表格的添加, 設置一些表格的屬性等等. 但是, 該方法是不好的, 因為需要為表單上的各表格創建類, 知道類的名字, 和表格都要有自己的處理程式 - 不可避免地許多代碼.

也可以在運行時完全用代碼創建表格物件並用自定義控制項裝配它. 但是, 在表格自己重構後, 你需要再次添加這些自定義控制項到表格中. 該方法用於表格重構不可避免的情形, 例如, 在管理性程式中 - 要在同一個表格中顯示任何表的內容, 但也需要一些象使用編輯框來編輯備註欄位和單擊列頭排序這樣的功能時.

不管表格如何捲動鎖住某列使其總是可見的

在需要鎖住某列而使表格捲動時都可見時, 要記住的第一件事是使用 split 表格. 當用戶捲動表格時左邊的列總是可見的. 但是, 這讓用戶看起來感到相當古怪, 額外的捲動條… 等等等等.
表格有一個很好的屬性 LeftColumn, 它是當前可視的最左邊列的列序號. 當表格橫向捲動時, 該值會改變. 我們可以在 Scrolled 方法中用它來鎖住某列:

if nDirection>3
this.Columns(1).ColumnOrder = this.LeftColumn
endif

另外, 對於用鍵盤捲動表格, 在 AfterRowColChange 添加一行:
this.Columns(1).ColumnOrder = this.LeftColumn
這樣第一列總是顯示在表格的最左邊!
 

表格訣竅

我所要說的第一個訣竅不僅僅是相對於表格的. 搜索 UT 站點關於表格的資訊. 你會找到許多有用的東西! 而且總是可以在這裏提出任何問題.
在使用 SQL SELECT 語句作為 record source 時, 總是在語句的後面添加 &INTO CURSOR …&, 否則將會在表單載入時或該查詢運行時顯示一個流覽視窗.

可以使用運算式作為 column source! 只需使列成為唯讀的並在列的 ControlSource 中寫入要顯示的運算式. 可惜的是, 表格中所有行中顯示的運算式的結果只能是相同資料類型; 否則你會得到奇怪的格式, 星號或一些其他奇怪的結果. 對於不同資料類型最好在表格列中使用容器…

要添加新控制項到表格列中, 在屬性視窗的下拉式列示方塊中選擇該列, 單擊正在編輯的表單的標題來選擇它, 選擇所需的控制項並拖動它到表格中. 要移去該控制項, 在屬性視窗中選擇該控制項, 單擊表單的標題來選擇表單, 然後按下 &Del& 按鈕.

在沒有橫向表格線時, 當 Sparse=.F. 或 HighlightRow=.F. 時, 游標所在的單格會顯示一個灰色的框. 放置一個白色的 shape 到表格下可以消去灰色的框.

可以在列的 MouseMove 事件中設置 mouse pointer 來改變滑鼠游標.


表格警告

本章為程式師提供關於表格的警告. 有很多…
*********

記住在設計時改變 record source 屬性時需要記住各列的 control source. 因為在 record source 改變後它們都會被清除! 在列的 Comment 屬性中創建一個 ControlSource 屬性的拷貝是一個不錯的辦法.

在表格被始化前, 不要讓它的 Record Source 屬性為空.

表格列中的控制項的事件和屬性在列的 sparse 屬性為 .T. 時僅作用於當前單格. 當 Sparse 為 .F. 時, 屬性用於顯示, 但事件也僅作用於當前單格.

Scrolled 事件只被 scrollbars 激發. 當表格是被鍵盤捲動或在特定情況下以編程方式捲動時, Scrolled 事件不激發.

RelativeRow 和 ActiveRow 屬性僅在表格具有焦點時可用. 在當前記錄超出可見的記錄時 ActiveRow 總是為零.

*********



VFP 中的表格 第二部分


在該部分中, 我將討論關於如何使表格開發更快速的列和列頭的竅門. 也將討論關於確定表格列頭和單格的確切位置. 象第一章中一樣, 我也包括了一些訣竅和警告.

表格列和列頭的竅門 - 如何使表格開發更快速

許多時候當你試圖創建一個簡單的應用程式來檢查是否有些事可以用表格來做或它將看起來是什麼樣的時候, 你會發現需要設置許多東西來使表格得到需要的外觀. 指定 RecordSource, 為各列指定 ControlSource, 指定各列頭的 caption 等等. 有時你又需要添加一些功能到表格中, 如象雙擊一個行以彈出別一個視窗 - 這也要求添加代碼到表格各列中的控制項中. 當只有一個只有少量列的表格在你的應用程式中時這不成問題. 但是, 當你的應用程式中有大量表格功表格有很多列時, 你會很快被各表格和列中的這些事情弄得疲憊不堪.

為表格和表格中的控制項定義類是容易的. 該方法得到了廣泛的應用. 但是, 還有更好的辦法來組織你的為表格使用的 OOP 的基類, 這將使得你的應用程式的表格編程有一個好的基礎.

首先, 定義一個提供所有功能和抽象方法的表格基類範本. 注意你應該保留你的類的 ColumnCount 屬性為預設值, 不要在表格基類中定義任何列數, 否在在類物件被實例時將很難處理它們. 調整表格的外觀為你的應用程式中最常用的情況. 例如, 在我的應用程式中通常我不在表格中顯示記錄標記 (Record Marks) (它們在表格不擁有焦點且當前記錄以另一種顏色高亮顯示時會不正確地刷新) 和刪除標記 (表格通常是唯讀的控制項).

然後, 定義用於表格列中的控制項. 通常它是一個文本框, 但你可能也想使用核取方塊, 下拉式列示方塊, 編輯框 (用於顯示備註欄位內容) 以及, 也許是一些其他不常用的自定義控制項. 你將只在表格內部使用這些控制項而決不會在其他地方使用這些控制項. 在這些類中你可以更好地組織所有表格列中需要的功能. 以調用表格基類中的抽象方法的方式來這樣做. 例如, 要調用雙擊方法, 在各表格列使用的類的 DblClick 事件中放入以下代碼:

This.parent.parent.eventDblClick(this.parent)

在以上示例代碼中 eventDblClick 是表格基類中的一個抽象方法 - 除接受單一參數的 &lparameters& 語句外它不包含其他代碼 (該參數是被雙擊的列的對象引用). 現在我們將從其中得到什麼呢? 沒有這個我們不得不為表格用雙擊代碼在表單上定義新的方法, 然後從表格各列中的控制項的 DblClick 事件中添加該方法調用. 當你有許多的列時, 這是相當令人厭煩的. 採用這種新的方法你只需要在你的表格基類的 eventDblClick 方法中寫你的代碼. 好了, 這也要求放置你自己的類到列中來代替默認的文本框, 但這太容易了. 放入以下代碼到你的表格基類的 Init 方法中:

LOCAL liColIndex
FOR liColIndex=1 TO this.ColumnCount && 遍曆所有的列
WITH this.Columns(liColIndex)
IF upper(.CurrentControl) == &Text1& && 只替換使用默認文本框的列
.Text1.Visible = .F.
.AddObject(&Text2&,&{GridTextBoxClass}&)
.Text2.Visible = .T.
.CurrentControl = &Text2&
.RemoveObject(&Text1&)
.Text2.Name = &Text1&
ENDIF
ENDWITH
ENDFOR

上述代碼中 {GridTextBoxClass} 是你的表格文本框類的名字. 注意如果你的保存該類的類庫沒有載入記憶體, 你將需要使用 NewObject 方法來代替 AddObject.

在進行了上述簡單的嘗試後, 讓我們來添加更多的東西. 定義更多的被表格列中的控制項調用的事件. 添加更多的你可能用於表格中的類, 然後用添加與列的 control source 的資料類型相適應的類的控制項來增強替換默認控制項的代碼(你可以用 type 函數來檢查列的 control source 的資料類型, 如: type(.ControlSource)). 通常核取方塊用於邏輯欄位而編輯框用於備註欄位.

現在我們的表格更易於在表單上使用了, 但是... 表格列頭怎麼辦? 一個好辦法是為表格列頭寫一些代碼來自動地用列的 control source 來寫入列頭的 caption 屬性. 例如, 在檢查了 ControlSource 是欄位而不是運算式後用 DBGETPROP(.ControlSource, &FIELD&,&Caption&) 來從資料庫中獲取欄位的 caption. 表格的 Init 中的示例代碼如下:

LOCAL liColIndex, lcCaption
FOR liColIndex=1 TO this.ColumnCount && 遍曆所有的列
WITH this.Columns(liColIndex)
&& 檢查列頭是不沒有被開發者設置
IF upper(.Header1.Caption) == &HEADER1&

&& 檢查 control source 是否是真正的欄位而不是運算式
IF &.& $ .ControlSource AND ;
FSIZE(substr(.ControlSource,at(&.&,.ControlSource)+1),.parent.RecordSource) > 0

&& 檢查別名是不是在 VFP 的資料庫中
IF !empty(cursorgetprop(&Database&,.parent.RecordSource)
lcCaption = DBGETPROP(.ControlSource,&FIELD&,&CAPTION&)
ELSE
lcCaption = &&
ENDIF
IF empty(lcCaption)
&& 沒如沒有其他情況只指定欄位名
lcCaption = substr(.ControlSource,at(&.&,.ControlSource)+1)
ENDIF
.Header1.Caption = lcCaption
ENDIF
ENDIF
ENDWITH
ENDFOR

這是最簡單的方法. 為了正確用 DBGetProp 獲取當前資料庫設置要求當前有一個打開的資料庫也是必須的.
你可能做得更多. 例如, 在使用欄位名作為列頭的 caption 時, 對它們進行一些複雜的轉換以使它們看起來更好一些, 對欄位名使用命令約定. 作為命名約定的示例如下: 在資料庫中所有除主關鍵字段外的欄位以資料類型字元為首碼. 主關鍵字中用 &ID& 來標識它. 因此我們可以從欄位名中移去第一個字元作為列頭的 Caption(當欄位名中不包含 &ID_& 或 &_ID& 或它本身來是 &ID& 時). 然後用 STRTRAN 替換所有下劃線為空格並用 &PROPER()& 函數來獲得最終結果. 採用該方法 &cCust_Name& 將在列頭中顯示為 &Cust Name&. 你可以為欄位使用其他的命名約定, 看看是否可以改進列頭的顯示. 如果你有 StoneField Database Toolkit (注:即眾所周知的 SDT) 或你自己的資料這典, 你可以查詢資料字典來獲得欄位的 caption 和任何其他的你想應用於列頭的和用於顯示資料的欄位屬性. 如果欄位來自視圖, 你可以從表中獲取它的源欄位來獲取它的屬性. 你可以從視圖欄位上用 "DBGETPROP(.ControlSource, &Field&, &UpdateName&)" 來從表中獲取欄位名. 這僅適用于可更新欄位, 作為運算式生成的欄位用上述方法返回的結果將是一個空串.

好了, 對於列頭的 captions - 你可以在表格基類的 Init 中設置它們然後忘掉它們. 但是, 還有一些有用的捕捉列頭的 Click 和其他事件事情. 在易於創建表格列控制項類時, 創建 header 類是相當難處理的. 你可以用相似的方法在你的表格基類中為列頭組織事件系統; 但是, 你可以在程式檔中用 DEFINE CLASS 結構以簡單的方法定義你自己的 header 類, 如下所示:

DEFINE CLASS MyHeaderClass AS HEADER
PROCEDURE DblClick
This.parent.parent.eventHeaderDblClick(this.Parent)
ENDPROC
ENDDEFINE

如果你熟悉 VCX 檔結構, 你可以在 VCX 庫中添加兩條記錄並指定其基類為 header 來創建 header 類. 但是這樣一來, 你除了象編輯 VFP 表一樣編輯它以外, 將不能再以其他方式編輯它. 因此最好還是首先在 PRG 檔中定義它用於開發和調試. 最後, 當它測試完成後, 如果需要的話你可以從 PRG 中移運它們到 VCX 中. 為什麼你會要它們在 VCX 檔中的理由是簡單的 - 你可以在設計時從控制項工具欄拖放它到表格列中, 這樣將在設計時以新的 header 類替換默認的 header. 我不知道還有其他的理由了... 對於運行時, 以下代碼需要添加到前述的遍歷代碼中 (改變默認控制項為你自己的控制項). 也為列頭類添加它們:

IF upper(.Header1.Class) == &HEADER& && 僅替換默認的 header 類
lcOldCaption = .Header1.Caption && 保存 caption 以應用它們到新的 header
.AddObject(&Header2&,&MyHeaderClass&) && 這將自動移除原有的 header
.Header2.Name = &Header1&
.Header1.Caption = lcOldCaption
ENDIF

你可能也想複製原表格列頭的其他屬性到你用新類創建的新的列頭物件. 也請注意到不需要移除原有的列頭因為 VFP 會自動移除它 - 來保持列中只有一個列頭. 對於 AddObject 定義列頭類的 PRG 檔必須事先用 SET PROCEDURE TO ?ADDITIVE 命令載入記憶體, 但是你可以決定用列的 NewObject 方法.

在一般情況下, 建議把替換表格中的控制項為你自己的類的代碼移動到一個單一的表格的方法中. 為什麼需要這樣呢 - 表格重構. 少數場合下表格重構是必需的. 在表格重構後你可以調用表格的方法來再次設置你自己的類, 因此即使當你在相同的表格控制項中顯示不同的資料時, 它仍然會以相同的方式運行. 這是因為, 儘管進行了重構, 事件的主要代碼是在表格控制項的方法中, 因此在重構後它們不會丟失. 更多的是, 在你運行了替換表格控制項為你自己的類的代碼後, 表格仍然具備所需的功能. 因此代替用不同的資料源創建多個表格類在同一個表單中來顯示這些不同資料源, 你現在可以放置一個單一的表格並用不會丟失太多功能的方法來重構它; 然後為每一個 data source 來創建新的表格類並增強不同數量的 record sources.

不好的事情是列頭的 Click 事件也會在表格列 resize 或移動時發生. 列具有 Moved 和 Resized 事件, 但是... 當你用自己的列頭類替換了默認的列頭類後, 很難使用列的這些事件, 而且它們的運行速度會變慢. 因此看來使用列頭類並不好. 但是有好的解決辦法. 在你的新的列頭類中比較 OldColumnWidth 和 OldColumnOrder 屬性. 然後在列頭的 Init 中設置它們為列的 Width 和 ColumnOrder 屬性的初始值. 在 Click 事件代碼中比較你的列頭類的當前的 Width 和 OldColumnWidth 屬性值及 ColumnOrder 和 OldCoumnOrder 屬性值. 當值與原值不同時, 則列是 resized 或 moved 了的. 在 resized 時, 只用當前列的 OldColumnWidth 來更新它. 當列 moved 時 - 你需要為列頭更新表格中的所有列的 OldColumnOrder 屬性. 好了, 在一定情況下列的寬度和列序現在可以用編程方法來修改了, 最好是不要直接這樣做, 而是使用表格類中的一個自定義方法調用, 例如, &ChangeColumnWidth(ColumnNumber)&, 或 &ChangeColumnOrder(ColumnNumber)&. 採用此方法你可以保證 OldColumnWidth 和 OldColumnOrder 總是與列的設置同步. 而且現在你有機會用上述方法在列移動或 resize 時來激發你的表格類的自定義方法中的代碼了 - 沒有真正接觸到任何列的方法!

為了加快輸入到列的 ControlSource 中, 你可以用簡單的方法. 在你的表格類中定義一個屬性 (ControlSourceList) 它是用於保存各列使用的欄位名的以分號分隔的列表. 使用分號而不是逗號是因為你可能會在一些 control source 中使用包含逗號的運算式. 在 Init 事件中分解該串到各屬性中(如果它不為空), 然後用串中的列表中的項填充各列的 ContolSource 屬性. 注意表格的任何刷新將不會先於該操作, 因為表格的初始化在默認情況下使用欄位在表中的物理順序 (列的重綁定 - 參見文章前面的描述). 當你的表格中有自定義控制項時, 可能列會使用對於該控制項來說是不正確的欄位類型並在刷新時造成不正確的資料類型錯誤 (Refresh 極少在表格的 Init 事件中發生, 但是, 在複雜的類代碼中有時可能會發生). 好了, 在設計時屬性表中的屬性長度限制是 255 字元, 因此在表格有許多列時將不能這樣做. 在此情況下當你在表單中使用表格時可以在表格的 Init 中用代碼來指定長的串到屬性, 然後用 DODEFAULT() 來調用父類代碼. 你也可以在串中不用欄位名前的別名來節約空間; 別名將被表格自動指定. 任何情況下, 在 ControlSource 中的運算式中的欄位名必須包括別名.

最後, 說說不同類型的 record source. 當 record source 是別名時, 沒有問題. 但是當你打開一個以表名以數值開始的表 record source 時, VFP 會以另一個別名來打開它. 表格中的 RecordSource 中的 SQL SELECT 語句也有類似情況. 幸運的是, 有一種簡單的方法來檢查表格使用的別名的正確性. 別名可以用任何列的 ControlSource 屬性來檢查. 表格會自動添加 "{alias}." 到所有列中的 control sources 欄位前. 你可以從列的 control sources 取出別名(當它們是簡單格式而不是複雜運算式時) . (如何檢查 control source 是否是一個欄位引用我已經說過了.) 好了, 當要用表格顯示一些用 SET RELATION aliases 關聯的表時, 這將不能工作, 但是, 顯示在表格中的關聯表通常使用別名作為 record source 類型. 那麼, 在我們的程式中用一個自定義屬性 (MainAlias) 來代替 RecordSource, 用它來保存表格使用的真實的別名. 我們也還定義了 RecordSource_Assign 代碼來捕捉 record source 的改變並更新我們的自定義屬性.


確切定位表格列頭和單格

表格中的控制項的功能可能與表格外的控制項的功能不同. 事實上關於這一點 VFP 有一些冗餘, 致使一些功能在表格中不能正確運行. 我不打算在這裏一一列舉, 但將說明一種好的方案 - 把控制項放在表格外並在表格單格上顯示它來進行資料編輯. 採用該方法控制項將象單獨的控制項一樣編輯資料, 而不會象表格列中的控制項一樣有一些功能不能使用. 現在對每一列採用此方法而你將不再受到表格的限制. 但是, 問題是複蓋在表格單格上的控制項的相對位置. 這就要求計算表格中的單格的準確位置以便用你的控制項來複蓋它. 這個事情不太好做, 列序可能改變了, 表格可能捲動了, 僅獲得焦點的表格可以計算行號等等. 在這裏我說明一種如何計算位置的一般方法.

另一個要求計算準確位置的理由是放置排序標記到表格列上來指明表格是按哪一個列進行排序的. 對於這一點隻要求計算列的位置.

位置的計算將在表格捲動後, 一些列寬改變後, 一個列移動後, 記錄標記和刪除標記的可視狀態改變後, 列高和行高改變後, 表格被分割後 (表格的 Partition 屬性設置為非零值). 你可以捕捉這些事件:

* 表格捲動在 Scrolled 事件中, 如果是用鍵盤產生的自動捲動在 AfterRowColChange 事件中. 因列移動造成的捲動 - 捕捉列移動或改變用 LeftColumn 屬性 (保存它們的原值). 其他原因造成的自動捲動則相當少見.
* 列的移動和寬度調整在列的事件中或用本文中前面描述過的方法
* 記錄標記和刪除標記的改變, 以及捲動欄的以編程方式的改變; 你可以在表格類中定義相應屬性的 Assign 方法來捕捉它們
* Partition, RowHeight 及 HeaderHeight 改變 - 在表格的 MouseUp 事件中用類似於前述的列頭方法來捕捉它們 - 保存原值到表格屬性中並在 MouseUp 事件中用新值比較它們以確定是否發生了變化.

因為要在許多地方計算來更新控制項的位置, 在計算列的位置時應該盡可能地快; 否則在表格中的列數及計算位置的控制項很多時會比較慢.

以下代碼中的演算法檢查列的大小和右邊沿. 它是最快的並經長期使用的方法. 它計算表格除 split 外的所有表格設置. 代碼中的注釋說明了演算法.

Procedure ColumnRightPosition
* returns position in pixels of right edge of column relative to the grid rectangle area
* returns 0 if column is outside of visible area of grid
* parameters:
* toColumn - reference to the column for which position should be calculated
lparameters toColumn

with this && "this" here is a grid reference
local lnIndex, lnColumn, lnColumnCountToLeft, lnPosition, lnVAreaWidth
local array laColumnsOrder(.ColumnCount)
&& fill array by column order numbers with column indexes in a single
&& array column. Note that we get only columns that could be visible and skip
&& columns that are not visible for sure (that have ColumnOrder= .LeftColumn
m.lnColumnCountToLeft = m.lnColumnCountToLeft + 1
m.laColumnsOrder[m.lnColumnCountToLeft] = ;
.Columns(m.lnColumn).ColumnOrder * 10000 + m.lnColumn
endif
endfor
m.lnPosition = 0

if m.lnColumnCountToLeft >0
&& truncate array from rest of unnessesary values
dimension laColumnsOrder(m.lnColumnCountToLeft)

&& Main trick here - use asort() VFP function. It will quickly
&& change order in array from column index to the order by
&& ColumnOrder. This will help us to reference each column
&& after LeftColumn in their visibility order.
asort(laColumnsOrder)

&& scan columns in the visibility order and increment width (result)
&& until we reach our column or right edge of the grid.
m.lnVAreaWidth = .Width - iif(.RecordMark, 10, 0) + ;
iif(.DeleteMark, 8, 0) + ;
iif(.ScrollBars>1,sysmetric(5),0) && Width of the visible portion of grid
&& note we now go through array of columns starting from left visible in order
&& of columns, i.e. first value appropriate to column .LeftColumn
for m.lnIndex = 1 to m.lnColumnCountToLeft
m.lnColumn = m.laColumnsOrder[m.lnIndex] % 10000
m.lnPosition = m.lnPosition + .Columns(m.lnColumn).Width + 1
if m.toColumn = .Columns(m.lnColumn) or m.lnPosition > m.lnVAreaWidth
&& so we will no need to scan remained columns
exit
endif
endfor

&& if we reached required column
if m.toColumn = .Columns(m.lnColumn)
&& if the column is last, its width is cut to fit into grid visible area
m.lnPosition = min(m.lnVAreaWidth,m.lnPosition)
if m.lnPosition > 0
&& add left grid edge controls width if they are shown
m.lnPosition = m.lnPosition + iif(.RecordMark, 10, 0) +;
iif(.DeleteMark, 8, 0)
endif
else
m.lnPosition = 0
endif
endif
endwith
m.toColumn = &&
return m.lnPosition

當前表格行的位置只有在表格獲得焦點時可以檢查到. 這是因為我們只能用表格的 RelativeRow 屬性來計算它, 當表格不擁有焦點時計算值總是零. 這種方法更簡單且更適於放入單個的運算式:

* 計算表格行相對於表格矩形區域的 top 位置, 單位是 pixels
* 如果位置在可視的區域外則返回零
Local lnRow
m.lnRow = this.RelativeRow
m.lnRow = this.RelativeRow
if m.lnRow=0
return 0
esle
return this.HeaderHeight + this.RowHeight*(this.RelativeRow-1)
endif

注意為了得到正確可靠的值, 處理中的表格獲得焦點 RelativeRow 屬性應該訪問兩次.
下圖顯示了表格屬性及它們的尺寸. 有助於更好地理解示例代碼.


表格訣竅

表格的 Format 屬性設置為 &Z& 時, 當 Sparse=.T. 時不顯示零值

是不是對表格列中顯示的備註欄位為 &Memo& 感到很厭倦? 你可以用類似於 "PADR({MemoField},200)" 這樣的運算式來顯示備註欄位. 也可以在列的 sparse 屬性設置為 .F. 時在列中用 EditBox 來允許對它們進行編輯, EditBox 可以比用運算式作為列的 ControlSource 時顯示更多的文本. 在表格中的 EditBox 中捲動欄僅在表格的 RowHeight 屬性大於三行文本的高度時才會顯示.

在表格可以完整地顯示所有列而沒有橫向捲動欄時, 你可以設置所有列的寬度小一些來讓所有的列完全顯示在顯示區中以避免當最後一列得到焦點時表格的自動捲動.


表格警告

在使用行緩存時刷新表格會造成資料的自動提交(Update). 這是因為在 VFP 內部, 表格會 scan 別名中的要顯示的記錄, 這會造成記錄指標的移動, 並因此造成資料的自動提交. 在用表格工作時, 別名應該使用表緩存模式.

在特定情況下, 當列的 ControlSource 返回的字元長於 200 字元且列的 sparse 屬性設置為 .T. 時表格會崩潰或不正常運行. 在此情況下用類似於 "PADR(...,200)" 這樣的表達工作為運算式來顯示資料. 如果要在這樣的情況下編輯資料, 在表格列中用 EditBox 來代替 TextBox 並設置列的 Sparse 屬性為 .F.. 當 SET DELETED 為 ON 時, 被刪除的記錄只有在記錄指標移動後才會不顯示出來. 在刪除後如果記錄指標在被刪除的記錄上時, VFP 保持記錄的可見, 包括在表格中.


VFP 中的表格 第三部分


在該部分中, 我將描述關於單擊列頭時表格排序, 表格的排序標記 (指示符) 和雙擊列頭時讓表格列的寬度與列中的資料寬度一致. 按照慣例, 將包括一些訣竅和警告.

單擊列頭排序

上面提到的單擊列頭排序表格和列表中的項是很流行的做法. 該功能在有成百上千的行列表中搜索某些東西時特別有用. VFP 的表格沒有內置該功能, 但可以編程方式來實現.

有多種現存的方法來達到此目的. 我們將不包含所有這些方法, 而只是說明最常用對於創建具有排序功能的表格類有用的方法. 這意味著描述的方法中將去掉它們中太複雜和不常用的部分. 建議將此功能作為你的應用程式中的表格基類的一部分這樣你可以在多個應用程式中重用它們.

首先, 準備一個列頭的 Click 事件來在正確的時候激發排序功能. Click 事件也會在列重調大小和移動時激發, 因此你需要使用一些特殊的類似于本文前面描述過的方法 - 使列頭類跟蹤列的重調大小和移動來區別它只是在單擊列頭. 當然, 這將要求表格中所有其他的東西支持並維護用新的列頭類替換默認的列頭類. 無論如何如果你想在任何表格中使用排序功能的話這是必需的. 當列是可調整大小的時候, 單擊 resize 區域而不調整列的寬度也會被捕捉到 - 因此在滑鼠的游標是 resizeg 箭頭是對列進行排序不是一個好的辦法, 這會把用戶搞糊塗 (我們也為本文稍後論及的另一個功能保留這一點). 表格列頭的 resize 區域是 11 象素寬 (列頭線的左邊 6 象素 4 象素右邊). 當你單擊 resize 區而沒有進行 resize 時, 僅管滑鼠此時已經在下一個列頭上, 滑鼠和單擊事件僅在可調寬度的列頭區被激發. 在示例中你可以看到在列頭類中是如何進行跟蹤的 - 使用 MouseUp 事件我們得到滑鼠指標的座標並檢查它是否只是在調整區域進行了單擊. 如果是, 設置特殊的標誌這樣在 Click 和 DblClick 事件中我們可以檢查滑鼠是否是在調整區.

一但純粹的單擊被從所有其他動作中分離出來後, 執行排序. 它由三部分組成 - 排序管理器, 排序常式和排序後的表格刷新.

排序管理器跟蹤當前要排序的是哪一個列, 為排序定義屬性關保存一個當前排序的狀態. 要這樣做, 使用了以下屬性:

Grid.SortedColumn - 包含當前排序列的索引如果沒有排序它將是零. 索引將用於快速跟蹤哪一個列是最後排過序的並在另一個列被為排序而單擊時關閉該列的排序 (儘管這不是必需的 - 排序列的索引將在一段簡單的遍曆所有列並檢查 SortingState 屬性值的代碼中被檢查).
Grid.lAllowSorting - 預設值為 .T. - 允許廢止對整個表格的排序.
header.lAllowSorting - 預設值為 .T. - 允許廢止對該列的排序.
Header.SortingState - 0 - 未排序, 1 - 按昇冪排序, 2 - 按降冪排序. 排序管理器將捲動這些值並在需要時調用排序常式.
Header.DefaultSorting - 該屬性決定在列頭被單擊時使用什麼樣的排序. 例如, 首選的日期排序在默認情況下是用降冪, 因此你可能想改變該屬性從默認的 1 到 2 - 這將得不斷單擊列頭時的排序的排序順序由 {無排序}->{昇冪}->{降冪} 變為 {無排序}->{降冪}->{昇冪}.
Header.lEventSwitchOffSorting - 一個事件 - 如果任何排序的特定處理造成了速度緩慢或錯誤, 關閉排序的屬性並去掉所有附加的索引. 它在以 Grid.SetAll("lEventSwitchOffSorting",.T.) 的格式調用被列頭中的該屬性的 _Assign 方法跟蹤. 該屬性在表格類中, 當 SetAll 函數被調用時表格的列頭還沒有準備好的情況下也是需要的 (否則該屬性未找到任何控制項時, VFP 有時會出現錯誤提示).
在最複雜的情況下列會從列格中移去, 因此 SortedColumn 屬性可能要求在進行這種處理後進行更新. 在這樣的情況下, 建議使用表格中的一個方法來掃描所有的列, 並用正確的, 包含非零排序狀態的列的索引來更新 SortedColumn 值. 好了, 你可以用另一種方法來跟蹤當前排序列的索引 - 使用一個返回當前排序列的索引的簡單的迴圈.

排序常式是該功能的主要部分. 有多種方法來實現表格中的資料的排序: - 保存資料到一個陣列並在表格中顯示陣列內容, 用 asort() 函數對陣列進行排序. 該方法有在些時候比較慢且限制了顯示的記錄數是陣列元素的最大值. - 為排序而用另一個選項重新查詢視圖或 SQL Pass-Through 結果集. 對於大的資料集來說這會比較慢, 但這是最簡單的多列排序方法. - 使用在運行時為結果集創建的索引或已存在的索引. 該方法最快因為它不保存任何中間的結果到記憶體中且不要求再次從伺服器查詢資料, 但要求編寫一些代碼.

在本文中我們只討論使用索引的方法. 其他的方法也可以用替換排序常式中的資料集來實現, 且該方法在排序後刷新表格.

在排序常式中使用了以下屬性:

Header.CurrentTag - 該屬性用於保存一個用於該列排序的標識名.
Header.SortingExpression - 當該屬性不為空時, 它的運算式將用於排序. 當它為空時, 排序常式將用列的 ControlSource 來創建排序運算式.
Header.SortingTag - 如果該屬性非空, 該屬性中的索引標識名將被認為已存在於 record source 中. 為什麼要在該欄位已經存在一個索引時還要在運行時創建一個索引標識呢?
最後兩個屬性給了排序編程以更多的選擇. 例如, 當一個表格包含 First Name 和 Last Name 兩個列時, 無論是哪一個列被單擊了, 按 First Name + Last Name 來排序是一個不錯的想法. 當然, 它可能要求額外的排序指示器和代碼, 當默認的排序常式不能適當地排序列時最好保持該屬性為空. 我們將在稍後討論多列排序.

排序常式檢查是否 SortignTag 或 CurrentTag 屬性非空, 並使用這些標識來排序. CurrentTag 在列物件的生存期間不會被清除, 或在要求刪除該臨時標識的特殊情況出現前不會被清除 (這會在設置 lEventSwitchOffSorting 屬性為 .T. 時發生). 這是可重用排序 - 我們只創建索引一次 (這會花一些時間), 保存創建的索引標識名到該屬性中, 然後在下一次需要排序時只需重用它而不再花時間. 當 CurrentTag 屬性為空時, 常式將為記錄源創建一個索引標識並保存標識名到該屬性中.

當指定了索引運算式時, 常式將只使用它而不進行驗證 (但基本的錯誤跟蹤仍然會進行). 否則排序常式將試圖檢查所有的詳情來創建運算式, 包括 NULL 值和 SET COLLATE 設置. 忘住最大索引運算式長度為 240, 但在 SET COLLATE 設置為非 "MACHINE" 時下降到 120. 索引不接受 NULL 值, 因此我們添加 NVL() 函數來假定沒有 null 值出現在結果集中. 對於長字串和備註欄位我們用 PADR() 函數. 最後, 當 ControlSource 值包含運算式而不是欄位時, 我們對字元值使用 PADR() 來保證運算式結果的長度總是相同的. 當然, 我們不能對通用欄位排序.

對於不同的記錄源進行索引還有一個重大區別. 對於視圖, 游標和 SQL Pass-Through 游標, 我們可以創建結構化索引標識而不會有任何問題, 因為這些索引標識產生的檔會在記錄源關閉時自動從磁片刪除. 但是, 帶表緩存的視圖不能用 INDEX 命令創建索引. 我們可以快速地臨時切換到行緩存, 對視圖進行索引, 然後再次設置緩存為表緩存. 但是如果視圖包含未提交的修改,改變緩存模式會造成錯誤. 好了, 你可以告訴用戶該表單上的該表格在資料被修改且未保存時不能排序, 或者只在打開視圖且用各列頭的 SortingTag 值來創建索引, 然後設置緩存模式為表緩存.

為一個表創建結構化索引不是一個好的辦法, 因為 ControlSource 中的運算式可能包含對其他欄位的引用並有可能是關聯表中的欄位, 因此在我們創建索引後, 其他打開表的用戶將因此而發生錯誤. 另外, 創建結構化索引要求對表的獨佔使用權. 對於這種情況, 我們使用保存在臨時 CDX 檔中的非結構化索引標識, 並以相同的名字作為索引標識. 這會產生一些其他可能的問題. 在資料工作期中包含了打開了非結構化索引的別名時, "BEGIN TRANSACTION" 不能啟動事務處理. 在該命令前你必須關閉所有非結構化索引. 另外, 最好是不要把包含上萬條記錄的資料集整個顯示給用戶. 準備一個篩選條件並從大的表中只選擇一個小的資料子集到表格中. 這樣會使速度更快, 並沒有直接訪問表的問題, 如象這種情況下的非結構化索引標識問題. 另外, 通過網路訪問來索引大的表速度會相當慢. 總之, 在你必須對表進行排序時, 盡可能地試著使用已經存在於表中的索引標識, 然後關閉表中沒有索引標識的列的排序. 另一個方案是 - 在開始事務處理前, 使用 Grid.SetAll("lEventSwitchOffSorting",.T.) 這樣表格將刪除可能存在的非結構化索引. 這將要求特別關注從使用表格的表單中調用的子表單中的事務處理.

當記錄源中包含 1000 以上的記錄時, 我們用列頭的 DispSortingMessage 方法來顯示資訊, 在默認情況下在排序期間顯示一個簡單的 "WAIT WINDOW" 資訊. 你可能準備用進度條來顯示索引進度或用一些其他方法來指明索引(排序)進度. 另外, 也可以在索引運算式中用自定義函數調用來顯示排序進程: 函數將被每一個被索引的記錄調用, 函數中的代碼更新圖形化的進度條 (在此情況下會稍稍降低索引速度).

當以該方法對視圖, 游標或 SQL Pass-Through 游標進行排序時, 排序速度是令人驚異的. 當表在本地磁片上時對表排序上很快的, 但通常通過網路訪問資料庫和它的表時速度是慢的. VFP SELECT 語句的結果游標也會被排序. 但是, 對結果集使用 NOFILTER 選項, 否則因為直接通過網路訪問表, 索引時會變慢 (另外使用非結構化索引不是一個好的辦法, 已在前面描述).

在排序後需要正確地刷新表格. 有這樣的情形, 改變排序的方向會給表格行顯示帶來強烈影響. 例如, 以昇冪排序, 將當前排序結果的第一條記錄放在第一行, 然後再次按降冪排序. 通常表格在這種情況下只顯示單一的行 - 降冪排序的最後一行, 在它的下面是空白的. 用戶在任何情況下可以使用捲動欄來使表格返回到好看的狀態, 但是, 最奇怪的事情是, 表格常常在記錄源中的所有的行都可以顯示下時顯示該行為. 假設這樣的情形: 當你看表資料源中所有的行都顯示在表格中, 單擊排序, 這時你只看到一行... 這通常會把用戶搞糊塗 - 為什麼在所有內容都顯示得下時表格會捲動? 要修正表格的該行為, 我們使用一個表格的額外的刷新: 設置記錄指標到當前排序結果中的第一條記錄, 然後再返回到當前記錄. 這假定當前記錄是在表的當前的可視區內或所有行都顯示在表格中. 這也可有更多的改進 - 當記錄在新排序的記錄源的尾部時, 用該方法顯示最後一條記錄(當表格底部有一部分沒有記錄的空白區域時). 要這樣做, 萬一記錄是在表格的底部, 移動記錄指標到頂部, 再移動它回到表格當前可視區中的記錄數減半的位置, 然後設置記錄指標回到希望的位置. 注意這只在記錄指標在接近最後一條記錄時有用. 向後和向前移動記錄指標在特定的記錄源中可能會比較慢, 特別是有很多的記錄或設置了篩選的記錄源時, 因此這不是一個好的通常情況下的辦法.

以上方法對一按單一的列排序是好的. 當你想按多列進行排序時, 你需要從當前加入到排序運算式中的且排序狀態非零的所有列中收集所有的排序運算式. 這是必需的, 因為用戶可能關閉了該部分列的排序以保證其他列的正確排序. 在此情況下, 重使用已存在的索引標識是非常困難的, 因為單個的列現在使用一些排序運算式. 通常這要求轉換所有的運算式為字元型並限制它們的長度不大於 240 字元 (或在使用了 比較序列時為 120 字元). 另外, 很難實現此種情況下的某列要求降冪排列而其他列使用昇冪排列的要求. 這要求以降冪轉換運算式的值. 對於不同的資料類型轉換是不同的. 例如, 數值型的值委容易用 "-" 操作符進行轉換. 在我們的情況中將只處理字元型的值. 在此情況下我們需要改變 "A" 為 "z" 等等. 你可以用 chrtran() 函數來快速地比較兩個串. 例如:

"Control" > "Binding"
chrtran("Control", cAllChars, cAllReverseChars) < chrtran("Binding", cAllChars, cAllReverseChars)

cAllChars and cAllReverseChars are prepared by following way:

cAllChars = ""
cAllReverseChars = ""
for nCharIndex = 0 to 255
cAllChars = cAllChars + chr(nCharIndex)
cAllReverseChars =cAllReverseChars + chr(255-nCharIndex)
endfor


因此, 例如, 索引運算式為兩列 Last name 和 First Name, 當按 Last Name 昇冪排列且 First Name 降冪排列時, 將看起來如下:
NVL(LastName,space(35)) + chrtran(NVL(FirstName,space(35)), cAllChars, cAllReverseChars)


在使用了比較序列的情況下(譯者注:即 SET COLLATE 設置為非 "MACHINE" 時), 問題會更複雜. 字元的排列與字元碼的序列不匹配, 因此串 cAllChars 和 cAllReverseChars 將以不同方式組成. 要改正這一問題, 創建一個只有一個字元欄位的表並用所有的字元填充它. 按使用的比較序列索引該表, 用已排序的表讀取所有字元到這些串中 - 這將保證這些串中的所有字元以正確的順序排列. 當串字元不是單字節時, 這些串變得太長(用於索引運算式中), 速度也因 chrtranC() 而慢下來. 好了, 按不同的排方向排序在使用視圖或查詢語句時可能用相當簡單的方法實現 - SELECT 語句的 ORDER BY 子句允許為各排序元素指定排序方向.
以下是一小點按多列排序的不同模式.

所有列交叉的共同排序, 如, First Name + Last Name - 排序一個列造成按組中的所有列排序
修正: 單擊列來排序它. 單擊第二個列添加排序到第一個排序中. 在此後單擊第一個列會只剩下第二個列的排序.
級聯: 兩個或更多的列加入到排序中, 當一個列(主列)排序時, 第二個列將與主列同時排序, 當主列未排序時, 單擊第二列致使主列自動排序. 在三列情況下第三列的單擊致使第二列和第一列(主列)自動排序. 有時複雜的模式允許一個以上的二級列.
動態: 與修正相似, 但所有列加入到一個單一的排序處理中, 因此用戶可以組織任何排序列的組合.
這可以用象 "cMultipleSortingColumns" 這樣額外的列頭屬性來實現, 該屬性是一個用於組合排序的列索引列表的串, 級聯排序模式也要靠它來排序 (優先排序號 - 在排序中哪一個是主列, 哪一個是次列等等). 對於動態排序不需要額外的屬性, 只需要另一個排序管理.
多列排序也要求特殊的方法來顯示排序指示符. 在此情況下要求一次顯示多個控制項來指出所有列的排序, 和當前排序的列. 對於動態排序指示器的數量將與表格中可以排序的列數匹配.

在示例中沒有如此複雜的多列排序的東西, 但是這裏討論的關於排序的大多數的東西.

表格排序標記 (指示器)

有多種方式向用戶顯示表格是按哪一列排序的. 最簡單的方法是改變列頭 caption 的背景色和前景色, 或者添加一個特殊的類似於箭頭的字元到列頭的 caption 來指出排序的列 (通常是字元 "^" (昇冪) 和 "v" (降冪). 這種做法看起來是可接受的但不是最好的. 可以用一個單獨的控制項來指出排序狀態, 但這並不是一件容易的事. 好了, 在表格列頭的 header 上放置類似於箭頭的控制項就象放置控制項到表格單格上一樣容易 - 所有尺寸也適用於這裏 (參見 VFP 中的表格第二部分). 但是, 在許多應用程式中你可以在列頭內看見一個小的箭頭. 在 VFP 是難於實現這樣的東西, 並且這要求一些特殊的方法. 這裏有兩種方法.

第一種是用 "DEFINE WINDOW ... IN WINDOW ... NAME ..." 命令使用單獨的視窗定義並用 Windows API 函數 SetWindowRgn() 來改變視窗形狀來使它看起來象一個箭頭, 並在箭頭下面使用一組線段控制項來顯示一些東西以模仿凹凸效果. 第二種方法是放置一個通常的透明容器控制項到表格面上並用一組線段來生成箭頭. 也可以用線段控制項, 簡單的在表單方法中在表頭上上繪出箭頭, 但這要求更多的努力來刷新這樣的排序指示器.

在表格列頭上放置一些東西的問題是非常困難的 - 表格列頭的自己重繪(redraw) 總是在其他 VFP 控制項的上面, 即使你用了 ZOrder 方法來放置你的控制項到所有其他控制項的上面. 有報告說在 W2K 作業系統上運行 VFP 的應用程式時不會出現該行為; 在 Windows NT 下它總是這樣. 一但特定的致使列頭刷新的事件發生時, 表格列頭將在其他控制項的上面自己繪製. 以下是這些事件和要求刷新排序指示器的事件的清單:

列頭的 click (在任何情況下) 和 right click (奇怪, 但的確在右擊時表格列頭移動到其他任何控制項).
表格的 refresh() 方法調用
改變當前單元 (AfterRowColChange 事件)
表格移動或重調大小或表格列頭被移動或調整大小後
表格捲動
列頭的高度調整後 (在表格的 MouseUp 事件中捕捉它)
表格失去焦點時 - 放置表格到容器中來捕捉它
好了, 所有這些東西, 正如你可以看到的一樣, 除最後一個外其他都可以用簡單的方法捕捉. 在表格列頭刷新後表格失去焦點不會激發任何表格事件. 它可以把表格放入一個容器中來捕捉它, 在一些情況下這樣做並不好, 尤其對於一般的表格類.

如果不會造成模式視窗的衝突, 視窗化的指示器是比較好的. 在一些情況下, 在模式表單中的另一個用 "DEFINE WINDOW ... IN WINDOW" 定義的窗口會造成許多方面的影響. 特別困難的是這樣的視窗不激發任何事件不能正確地被滑鼠單擊 (表格列頭的排序指示器需要滑鼠單擊). 對於無模式表單這樣的視窗獲得焦點, 也不是好事情, 因為會激發許多事件而且這需要特殊的處理.

在代碼中從多個地方刷新排序指示器, 使用了單獨的表格類的方法. 刷新計算正確的指示器位置並放置它. 它包含了刷新指示器的方法 - 指明不同的排序方向, 並適當地設置箭頭的顏色為列頭的背景色. 顏色計算使用顏色亮度屬性百分比來實現顯示線段來模仿凸起效果. 它也保證列頭的背景色很淺或很深時箭頭也是可見的.

只在運行時使用指示器控制項是個不錯的辦法. 在表格中, 它創建於 Init 事件並在表格的 Destroy 事件中清除. 對於多列排序你可能要使用一個陣列, 用於保存為排序列創建的所有指示器控制項的引用, 或在列頭中按排序引用為各列創建的指示器.

在雙擊時以資料寬度重調表格列寬

你可以在帶有表格或列表的應用程式中看到一個有用的功能 - 雙擊列頭的 resize 區自動調整列寬為列中顯示的資訊的寬度. 在示例中你可以看到在列頭的 DblClick 和 MouseUp 事件中我們如何在列頭的 resize 區從其他的事件中區分開雙擊. 這會激發 auto-resizing 演算法. 它從 controlsource 中得到欄位大小, 並用重複欄位大小次數的字元 &O&(它擁有字元的平均寬度)和 TxtWidth() 函數計算列的象素寬度. 特殊欄位如備註欄位和帶有長空格的字元欄位. 另外, 列的 ControlSource 運算式不能給出要顯示的最大字元數. 不擴展列寬為最大大小是一個好的辦法, 只擴展為列中已有資訊的寬度 - 要適應大多數行資訊而不佔用更多的列寬. 對於這種情況, 掃描當前記錄位置附近的記錄並計算 TxtWidth() 函數返回的運算式結果的最大值. 該值將成為列的寬度. 但是, 如果當前記錄附近的所有行的值都是空值, 最好指定一個默認的列寬, 如, 在列頭的 DefaultWidth 屬性中 (它也可用於一些其他場合).

表格訣竅

你可以用容器控制項在表格單格中顯示任何東西. 要刷新各行中的容器, 在 Dynamic* 屬性的運算式中你可以使用函數調用. 函數將被各表格行調用, 因此在該函數代碼中你可以修改容器中的任何東西並刷新它. 在調用這樣的函數時記錄源中的記錄指標是在正確的位置上.

要產生多行表頭, 在 VFP7 中使用列頭的 WordWrap 屬性. 在 VFP6 中多行表頭可以用複蓋在列頭上的標籤代替, 或類似於表格列頭的容器控制項 (可以在 Universal 的下載節中下載這樣的示例).

要為表格的不同部分顯示 tooltip, 在 timer 事件中用 MROW() 和 MCOL() 函數檢查滑鼠在特定時間內是否沒有移動並顯示你自己的控制項. 表格的部分可以用表格的 GridHitTest 方法來檢查. (多行/奇特 ToolTip 控制項以該方式處理, 你可以為該用途修改它; 可以在 Universal 的下載節中下載這樣的示例).

表格警告

不要用表格列控制項的 "Value" 屬性進行計算. 要從計算中得到資料, 直接訪問表格的別名. 這是因為在非活動列中的控制項通常用於表格外觀的刷新, 並因此它的 Value 屬性對於當前行的值是不正確的.

列頭總是表單上的其他 VFP 控制項上而不管它是被如何安排或放置的. 使用單獨的視窗控制項也有一些缺點.

用 RecCount() 和當前 RecNo() 計算縱向捲動欄位置, 不會真正地顯示記錄數. 當記錄源是經篩選的或包含大量有刪除標記的記錄時, 捲動欄常常會以不正確的位置讓用戶糊塗. 在此情況下建議使用查詢或視圖.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多