15. 資料表與報表
DataCombo與DataList控制項
Visual Basic 6包含了兩個可使用資料連結的控制項:DataList與DataCombo。這兩個控制項不同於ListBox與ComboBox,因為其可以與兩個不同的ADO資料控制項進行資料連結。第一個資料控制項決定被選取的資料(就像ListBox與ComboBox般),另一個資料控制項填滿清單資料。
DataList與DataCombo控制項常被用來做查閱表(Lookup table)。查閱表是種表格,其包含的資料項有易讓人了解意義的描述且每個資料項有相對應的編碼值。例如:在Nwind.mdb的Products資料表有個CategoryID欄位,此欄位值對應到Categories資料表的CategoryID欄位。因此,為了顯示產品的資訊,必須以INNER JOIN語法來找出所有需要的資料:
SELECT ProductName, CategoryName FROM Products INNER JOIN Categories ON Products.CategoryID = Categories.CategoryID
當透過程式碼來存取資料時,此方法可用;但當使用連結控制項時卻成了無用的方法。例如,若使用者被允許修改產品的型錄時會怎麼樣?於此案例,自我驗證的界面將需要把Categories資料表內的所有值載入到ListBox或ComboBox控制項,以便讓使用者無法鍵入不正確的型錄名稱。這項工作需要開啟第二個Recordset,如下列程式碼:
' This code assumes that cn already points to a valid connection. Dim rsCat As New ADODB.Recordset RsCat.Open "SELECT CategoryID, CategoryName FROM Categories", cn LstCategories.Clear Do Until rsCat.EOF lstCategories.AddItem rsCat("CategoryName") lstCategories.ItemData(lstCategories.NewIndex) = rsCat("CategoryID") rsCat.MoveNext Loop RsCat.Close
當然,當使用者要找出Product資料表中某筆資料時,需要撰寫程式找出正確的資料項目,而當使用者選取不同項目時,程式會修改Products資料表的CategoryID欄位值。這看起來如此簡單的工作卻可能需要撰寫不算少的程式碼。幸運的是,只要在設計階段,設定幾個屬性,則DataCombo與DataList控制項便能很輕易地達成這樣的結果。
DataCombo與DataList控制項包含於MSDATLST.OCX檔內,因此若您引用到這些控制項,就必須要隨著應用程式散發這個檔案。
說明
DataCombo與DataList控制項的功能類似在Visual Basic 5內所附的DBCombo與DBList(其在Visual Basic 6依然有提供)。主要的不同點在於DBCombo與DBList控制項是針對舊式的資料控制項與RemoteData控制項,而DataCombo與DataList控制項則僅支援ADO資料控制項。
設定設計階段屬性
要用DataCombo與DataList控制項來實作查閱表,需要在表單上放置兩個ADO資料控制項,一個指向主要的資料表(如上例中的Products),另一個則指向查閱表(如上例的Categories)。然後最少設定下列屬性:
底下讓我們來實作一下試試。首先建立一個ADO資料控制項(Adodc1),將之指向NWind.mdb的Products資料表,然後增加幾個TextBox以資料連結的方式來顯示幾個欄位值。再產生另一個ADO資料控制項,並將之設定成可自Categories資料表獲得資料,最後,增加一個DataList控制項並設定其屬性如下:DataSource = Adodc1,DataField = CategoryID,RowSource = Adodc2,ListField = CategoryName,與BoundColumn= CategoryID。
Products資料表包含另一個外鍵(SuppliersID),其指向Suppliers資料表。因此,還可以在做出另一個查閱表,只要再增加第三個ADO資料控制項(Adodc3),將之指向Suppliers資料表,且增加另一個DataCombo控制項,設定其屬性為:DataSource = Adodc1,DataField = SupplierID,RowSource = Adodc3,ListField = CompanyName,與BoundColumn = SupplierID。執行結果如
圖15-1 。說明
DataCombo與DataList控制項包含兩個額外的屬性-DataMember與RowMember,其只有當使用DataEnvironment設計師的Command物件作為主要或次要資料表時方有用處。
DataCombo與DataList控制項還有其他設計階段的屬性,但絕大部份都與ListBox和ComboBox的屬性相同,於此就不再贅述。唯一一個您必須了解的是MatchEntry屬性,其值可以為0-dblBasicMatching或1-dblExtendedMatching。在基本比對模式(0-dblBasicMatching)下,當焦點在控制項時,使用者按下按鍵後,控制項會將光棒移到以所按下字元為開頭的資料項上。在延伸比對模式下,每次所鍵入的字元會形成一個搜尋字串,然後會以此搜尋字串來尋找資料項,並將光棒移到適當的資料項上。(搜尋字串會經過幾秒後會自動重設,或按下倒退鍵亦會重設。)
如同所有的資料連結控制項,DataCombo與DataList亦包含DataFormat屬性,但此屬性所產生的結果會與您所預期的不同。例如:您無法使用DataFormat來改變資料項目的顯示格式。然而這並非臭蟲(Bug)所在,DataFormat僅對DataField欄位有效,而此欄位在這些控制項中是被隱藏住的。因此,這兩個控制項的DataFormat屬性是受到限制的。下述的小技巧會告訴您如何將改變資料項目的顯示格式。
小秘訣
經常您需要在DataCombo或DataList控制項的清單中顯示組合的欄位資料。例如:可能要同時顯示廠商與城市的名稱。可以將第二個ADO資料控制項的RecordSource欄位帶以算術型的SELECT語法來達成:
Adodc3.RecordSource = "SELECT SupplierID, CompanyName + ' (' " _ & "+ City + ')' AS NameCity FROM Suppliers"
別忘了將組合過的欄位取個別名,否則無法用在BoundColumn屬性上。也可用相同的技巧來排序這些資料項。例如,可以排序廠商並將其全部大寫化,如下述語法:
Adodc3.RecordSource = "SELECT SupplierID, UCase(CompanyName + ' (' " _ & "+ City + ')') AS NameCity FROM Suppliers ORDER BY CompanyName"
圖15-1 有著兩個查閱表的資料連結表單 |
執行階段作業
DataCombo與DataList控制項於執行階段的操控方式很類似ListBox與ComboBox,但仍有些部份差異。例如,沒有ListIndex與ListCount屬性,也沒有AddItem方法來增加項目。唯一將資料項填入清單的方式是藉由ADO資料控制項或其他資料來源,例如Recordset或DataEnvironment。
DataList與DataCombo控制項還包含其他屬性。如果DataCombo控制項的編輯區內的值為清單區內的某一項時,MatchedWithList唯讀屬性的值便為True。當DataList與DataCombo控制項的Style屬性為2dbcDropDownList時,這屬性總是為True。BoundText屬性傳回或設定由BoundText屬性所決定的欄位值(這表示其值將會填入DataField欄位內)。
顯示額外的查閱資訊
SelectedItem屬性傳回在清單區中所選取的資料項目。此屬性常被用來顯示關於被選取項目的相關資訊。例如假設有一個新廠商於清單中被選取時,您要從Suppliers資料表中顯示ContactName欄位值。為了達成此目的,建立一個Label控制項叫lblSupplierData且將下列程式碼加到表單模組內:
Private Sub DataCombo1_Click(Area As Integer) ' Move to the correct record in the lookup table. ' 注意事項: The ContactName field must be included in the list ' of fields returned by the Adodc3 data control. If Area = dbcAreaList Then Adodc3.Recordset.Bookmark = DataCombo1.SelectedItem lblSupplierData = Adodc3.Recordset("ContactName") End If End Sub
DataCombo的Click與DblClick事件接收到Area參數,其指出控制項的哪個區域被按到。可能的值有:0-dbcAreaButton、1-dbcAreaEdit、2-dbcAreaList。
上述方法的問題是當使用者顯示一個新紀錄時,DataList或DataCombo控制項的Click事件並不會被引發。因此,必須在ADO資料控制項的MoveComplete事件中做處理:
Private Sub Adodc1_MoveComplete(ByVal adReason As ADODB.EventReasonEnum, _ ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) ' You need to manually assign a value to BoundText because the ' SelectedItem property hasn't been updated yet when this event fires. DataCombo1.BoundText = Adodc1.Recordset("SupplierID") ' Simulate a Click to keep the control in sync. DataCombo1_Click dbcAreaList End Sub
VisibleCount屬性傳回清單區可視項目的數量。其常與VisibleItems屬性一起使用,其傳回指向查閱表(對應到清單區的所有可視項目)的書籤(Bookmark)陣列。例如,您可以放置一個名為lstDescription的ListBox控制項於DataList1控制項的右側,且以查閱表的額外資訊填入,如下列程式所示:
Dim i As Long lstDescription.Clear For i = 0 To DataList1.VisibleCount - 1 Adodc2.Recordset.Bookmark = DataList1.VisibleItems(i) lstDescription.AddItem Adodc2.Recordset("Description") Next
當一筆新資料成為現行資料時,可執行此程式碼,但要保持lstDescription ListBox控制項與DataList1控制項同步會有問題,因為後者缺乏Scroll事件。一個使用VisibleCount與VisibleItems較好的方式為實作ToolTip機制:
' This code assumes that DataList1.IntegralHeight = True. Private Sub DataList1_MouseMove(Button As Integer, Shift As Integer, _ x As Single, y As Single) ' Determine the item over which the mouse cursor is placed. Dim item As Long item = Int(y / DataList1.Height * DataList1.VisibleCount) ' Retrieve the description for the category under the cursor, and ' prepare a Tooltip in case the user doesn't move the mouse. Adodc2.Recordset.Bookmark = DataList1.VisibleItems(item) DataList1.TooltipText = Adodc2.Recordset("Description") End Sub
注意
當使用由ADO資料控制項所延伸出來的Recordset或直接資料連結到資料感知控制項的Recordset的屬性與方法時,可能會導致錯誤H80040E20。可使用Static客戶端指標來消除此錯誤,或藉由執行下列程式來消除:
ADODC1.Recordset.Move 0
要查詢更多的資訊,請看Microsoft Knowledge Base第Q195638號文章。
儲存Connection
舊有資料控制項(已由較新的ADO資料控制項所繼承)的問題之一為,控制項的每個實體皆開啟其各自的連結到資料庫,這樣會有兩個不好的結果。第一,若有多個資料控制項,其無法共享同一個交易空間。第二,每個連線會佔據伺服器的資源。若一個表單使用許多查閱表(LookUp Table)建築在DataCombo與DataList控制項上,則應用程式將耗盡資源,且若可用的連線不足時將導致許多問題。
當運用ADO資料感知控制項時,常要避免這些浪費資源的事情。事實上,DataCombo與DataList並不一定需要一個可視的資料控制項,因為使用者很少真的用來導覽查閱表。因此,可用單純的ADO Recordset物件達成同樣的結果。設定DataCombo與DataList控制項的屬性如同其與ADO資料控制項進行資料連結般,只要保持RowSource屬性值為空白。在建立共用ADO資料控制項連線的Recordset物件後,才於執行階段設定此屬性:
Dim rsCategories As New ADODB.Recordset Dim rsSuppliers As New ADODB.Recordset Private Sub Form_Load() rsCategories.Open "Categories", Adodc1.Recordset.ActiveConnection Set DataList1.RowSource = rsCategories rsSuppliers.Open "Suppliers", Adodc1.Recordset.ActiveConnection Set DataCombo1.RowSource = rsSuppliers End Sub
更新查閱表(Lookup Table)
到目前為止,我們均假設查閱表的內容為固定的。然而實作上,當使用者新增一項產品,其來自一個不在Suppliers資料表內的廠商時,會需要新增一筆資料到資料表內。可將DataCombo控制項的Style設為0-dbcDropDownCombo來處理這個狀況。當主要的ADO資料控制項即將寫入值到Products資料表時,程式會檢查是否Supplier名稱已存在於Suppliers資料表內,若未存在,則詢問是否新的廠商要被建立。底下視實作此方法的最少程式碼:
Private Sub Adodc1_WillChangeRecord(ByVal adReason As _ ADODB.EventReasonEnum, ByVal cRecords As Long, adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset) ' Exit if data in DataCombo hasn't been modified ' or if it matches an item in the list. If Not DataCombo1.DataChanged Or DataCombo1.MatchedWithList Then Exit Sub End If ' Ask if the user wants to add a new supplier; cancel operation if not. If MsgBox("Supplier not found." & vbCr & "Do you want to add it?", _ vbYesNo + vbExclamation) = vbNo Then adStatus = adStatusCancel End If ' Add a new record to the Recordset. In a real application, you should ' display a complete data entry form. rsSuppliers.AddNew "CompanyName", DataCombo1.Text rsSuppliers.Update ' Ensure that the new record is visible in the Recordset. rsSuppliers.Requery rsSuppliers.Find "CompanyName = '" & DataCombo1.Text & "'" ' Refill the DataCombo and make the correct item the current one. DataCombo1.ReFill DataCombo1.BoundText = rsSuppliers("SupplierID") End Sub
上述程式碼自動地以簡單的方法加新紀錄到Suppliers資料表;真正的應用程式應該顯示完整的資料項目表單,以便讓使用者鍵入關於新紀錄的相關資料。
DataGrid控制項
可能最常用來顯示資料表內資料的方式為使用Grid控制項。Visual Basic 6附有幾種Grid控制項,但只有兩種可搭配ADO資料控制項與其他的ADO資料來源:DataGrid控制項與HierarchicalFlexGrid控制項。本節將說明DataGrid控制項的用法,而下一節則解釋Hierarchical FlexGrid。
在了解DataGrid控制項的屬性、方法與事件前,得先了解其物件模型。如圖15-2所示,這是個簡單的物件模型,其中最頂層為DataGrid控制項,然後Columns與Splits集合在其下。可以把DataGrid控制項分成多個區域,並使其各自獨立或互相同步。DataGrid控制項被涵蓋於MSDATGRD.OCX檔內,因此當應用程式散佈時需包含此檔案。
圖15-2 DataGrid控制項的物件模型 |
注意
DataGrid與早期的DBGrid控制項在程式碼方面是相容的,雖然DBGrid在Visual Basic 6內仍有包含,但卻步支援較新的ADO資料控制項羽資料來源。也由於相容性,因此DataGrid控制項方能迅速地取代DBGrid控制項。兩者間唯一的不同是DataGrid控制項不支援非資料連結模式。但因為可以連結此控制項至任何ADO資料來源-包含您自己的類別,18章會詳述-所以可以建立涵蓋記憶體內資料結構(例如UDT陣列或二維的字串或陣列等)的類別。
設定設計階段屬性
由於DataGrid控制項僅能以與ADO資料來源進行資料連結的方式取得資料,所以首先得準備一個資料來源。其可以是個設計階段的來源,例如ADO資料物件或DataEnvironment物件,要不也可是個執行階段的物件,例如ADO Recordset或是個自定資料類別的實體。透過設計階段的來源是較為恰當的,因為如此可以在設計階段不需撰寫程式便取得欄位結構、欄位寬度與其他屬性。
注意
可以將複雜的控制項(例如DataGrid與階層式FlexGrid控制項)連結到Static或Keyset指標的Recordset。
編修資料行配置
再把DataGrid控制項透過DataSource屬性連結到ADO資料物件或DataEnvironment的Command物件後,以滑鼠右鍵按一下該DataGrid控制項,然後按一下 擷取資料欄 。如此會在設計階段完成欄位的安排,包括每個欄位的標題與寬度等皆會從資料表內對應的欄位得到。接下來再以滑鼠右鍵按一下該DataGrid控制項,然後按一下 編輯 ,會進入編輯模式。在此模式下,可以調整欄位寬度、透過下方的捲軸水平地捲動Grid,並再以滑鼠右鍵按一下該DataGrid控制項會顯示一個選單。這些選項允許您增減欄位、把Grid拆成多個區塊、剪下貼上欄位以便調整其順序等。然而,要修改其他屬性,必須再以滑鼠右鍵按一下該DataGrid控制項,然後選擇 屬性 選項,其會開啟有八個頁籤的屬性頁,如圖15-3。
DataGrid內不同的區塊有可能有不同版面配置(這與說明文件所言相反)。事實上,若在一個區塊中刪除一個存在的欄位或增加一個新欄位,其他區塊都會被影像到。可採取的方式為設定欄位的Visible屬性為偽。因為此屬性是各區塊獨立的(於本章的版面配置頁籤中會說明),所以可以在所有區塊中隱藏某不想顯示的欄位。
圖15-3 設計階段的DataGrid控制項,以及其屬性頁 |
一般與鍵盤頁籤
Grid預設情況下是沒有標題的,但可在屬性頁的一般頁籤中輸入自訂的字串;如果Caption屬性有值的話,會在資料行首上顯示一個灰色標題區域。AllowAddNews、AllDelete、AllowUpdate為布林屬性,決定哪些作業是可被允許的。ColumnHeaders屬性設成偽(False)會隱藏資料行標題。注意在字型頁籤,可設定HeadFont屬性控制標題行的字型。
DelColWidth屬性是Grid資料行的預設寬度:若設成0(預設值),每行的寬度為其資料行標題寬度與其下寬度的最大值。HeadLines屬性值為0至10,對應到資料行標題的列數;可設成0表移去資料行首,但把ColumnHeaders屬性設成偽也可達到同樣的效果也較適宜。RowHeight屬性是每列的高度,單位為twips。DataGrid不支援不同高度的資料列。
BorderStyle屬性設為0-dbgNoBorder則不會在Grid四周顯示固定邊框。RowDividerLine屬性決定分隔線(列與列間)的樣式,其值可能有:0-dbgNoDividers、1-dbgBlackLine、2-dbgDarkGrayLine(預設值)、3-dbgRaised、4-dbgInset或5-dbgUseForeColor。若設定成3-dbgRaised或4-dbgInset,分隔線的顏色決定於Microsoft Windows的設定。
鍵盤頁籤讓您設定關於按鍵的屬性。若AllowArrows屬性為True,使用者可用鍵盤方向鍵來巡覽儲存格;若WrapCellPointer也為True,於某列的最後按下右方向鍵時會將駐點移到下列的第一個儲存格;而在某列的起始按下左方向鍵則會將駐點移到前一列的最後一個儲存格。
TabAction屬性決定Tab或Shift+Tab鍵按下時的行為。預設值是0-dbgControlNavigation,表示下一個控制項會接收到駐點(若是按下Shift+Tab鍵,則為前一個控制項)。若設成1-dbgColumnNavigation,按下Tab鍵則駐點移到下一行,除非目前位處該列的最後一行儲存格(若為Shift+Tab被按下,則為第一個儲存格)。按下這些鍵時會讓駐點依照TabIndex的順序來移動到下一個或前一個控制項。最後,2-dbgGridNavigation很類似前一個,按下Tab鍵不會移動駐點至下個控制項,而當駐點在列首或列末時則根據WrapCellPointer屬性而定。
Tab與方向鍵預設不會將駐點移到同一個Grid的其他Split。然而,設定TabAcrossSplit屬性為真則可讓使用者藉由Tab鍵來巡覽Split。在這種情況下,WrapCellPointer與TabAction屬性會被忽略,除非當駐點在最右分割群組的最後一行時按下Tab鍵或在最左分割群組的第一行時按下Shift+Tab鍵。
資料行與格式頁籤
資料行頁籤允許您設定每個Column物件的Caption屬性、DataField屬性(包含資料連結的各欄位名)。格式頁籤則可設定每個Column物件的DataFormat屬性。一般來說,藉由此頁籤來格式數字、錢幣值、日期與時間。若需要的話也可自訂格式。於此頁籤所設定的資料會於執行階段反映在每個Column物件的DataFormat屬性。Column物件的其他屬性(稍後會解說),也可在格式頁籤設定。
分割群組頁籤
若Grid被分成多個分割區域時,可於分割群組頁籤設定這些區域的屬性。於屬性頁無法建立新的分割,但卻可在此屬性頁中設定每個分割的外觀等。(建立新的分割於本章先前的
〈編修資料行配置〉 一節中有提到。)為了改變分割的屬性,必須先從上方的下拉選單選擇一個分割群組。若Grid沒有分割的話,於選單中只有一個項目-Split 0項目-且您的設定會影像到整個Grid控制項。把Locked屬性設為真會讓DataGrid成為唯讀的控制項。AllowFocus屬性決定是否分割可獲得駐點(這很類似Visual Basic控制項中的TabStop屬性)。AllowSizing屬性決定是否可用滑鼠於執行階段改變列的大小。(Resize作業會影響所有分割的所有列,因為DataGrid控制項不支援不同高度的資料列。)RecordSelectors屬性決定是否要在分割或整個Grid的左側顯示一個灰色的資料行用來選擇資料用。
可以控制是否多重分割區要一起垂直捲動或獨立捲動,藉由Split物件的ScrollGroup屬性,其是一個大於或等於1的整數。有著相同值得分割會一起捲動,所以可藉由設定不同值給此屬性來建立獨立捲動的分割。ScrollBars屬性決定在特定的分割中是否要顯示捲軸,其值可以為:0-dbgNone、1-dbgHorizontal、2-dbgVertica、3-dbgBoth與4-dbgAutomatic。(預設值為4-dbgAutomatic-只有當需要時才顯示捲軸。)若有一群一起捲動的Split物件,且每群的ScrollBars屬性為4-dbgAutomatic,只有每群最右側的分割會顯示垂直捲軸。
MargueeStyle屬性決定DataGrid控制項如何對待目前選取的儲存格。此屬性值可以為:0-dbgDottedCellBorder(儲存格周圍為點狀邊框,也叫做駐點矩形),1-dbgSolidCellBorder(固定邊框,比起點狀邊框要來得醒目),2-dbgHighlightCell(文字與背景色會反向),3-dbgHightlightRow(整列被高亮化-只有當Grid或分割是不可修改時才有用),4-dbgHighlightRowRaiseCell(類似前一個,帶只有現行儲存格被高亮化),5-dbgNoMarquee(現行儲存格不論如何皆不會高亮化),或6-dbgFloatingEditor(預設值-現行儲存格會使用有著閃爍游標的浮點編輯視窗來高亮化,如Microsoft Access般)。
DataGrid所有的AllowRowSizing、MarqueeStyle與RecordSelectors屬性,在Split物件中也有。其在Split物件中的作用與在DataGrid控制項中相同。
在分割頁籤的最後兩個屬性是一起運作的,其決定此分割中有多少行要顯示,且他們是否要改變大小使之能容納在可視區域內。更精確地說,Size屬性是個數值,其意義由SizeMode而定。若SizeMode為0-dbgScalable,則Size值關連到相對於其他分割的寬度;例如,若有兩個分割,其Size分別為1與2,第一個分割寬度會是Grid寬度的1/3,而第二個則為2/3。若SizeMode為1-dbgExact,則Size是關連到分割實際寬度的浮點數(單位為twips),此設定能保證分割總是有相同的寬度,不論其他分割被移除或新增。
版面配置頁籤
在版面配置頁籤,可設定每個分割群組中資料行的屬性。事實上DataGrid控制項允許在不同的分割群組,針對相同的資料行有著不同的屬性。例如、在某分割群組中一資料行是可讀寫的,而在另一群組則是唯讀的;或在某些分割群組,一資料行是看不見的,而在另一群組則為可視的。用Locked屬性設定唯讀屬性,而用Visible屬性決定可視性。AllowSizing布林屬性決定是否資料行的右邊界可移動以便改變資料行的寬度。WrapText布林屬性在需要時可讓儲存格內的文字換行:還可用RowHeight屬性來產生多行的顯示。Button屬性若設為真,則當某儲存格獲得駐點時,會顯示一個下拉選單的按鈕。當使用者按下此按鈕時,DataGrid控制項接收到ButtonClick事件,此時可用標準的ComboBox、ListBox甚至DataGrid控制項來顯示一列列的值供選擇。
DividerStyle屬性決定資料行右邊界的垂直線的樣式,其值可能有:0-dbgNoDividers、1-dbgBlackLine、2-dbgDarkGrayLine(預設值)、3-dbgRaised、4-dbgInset、5-dbgUseForeColor或6-dbgLightGrayLine。Alignment屬性決定資料行內容的排列情形,值可能有:0-dbgLeft、1-dbgRight、2-dbgCenter或3-dbgGeneral。(預設情況,文字是靠左對齊而數字是靠右對齊的)。最後,Width屬性為每個Column的寬度。
執行階段操作
DataGrid控制項是複雜的並且可能需要花較多時間才來瞭解它。對於您可能想要操作的方面筆者會概述大部分一般的操作,並提供此物件的一些技巧。
關於現行儲存格
DataGrid控制項在執行階段中最重要的屬性是Row和Col,他們設定或傳回駐點儲存格(Cell)的位置。第一列和最左邊的行傳回零值。一旦您使給定的Cell成為目前的Cel,使用DataGrid控制項的Text屬性可以獲得和修改它的內容。
' Convert the current cell's contents to uppercase. Private Sub cmdUppercase_Click() DataGrid1.Text = Ucase$(DataGrid1.Text) End Sub
若目前的Cell在編輯中,EditActive屬性會傳回True,否則為False;也可以用程式來指定值給此屬性以便進入或離開編輯模式。當進入編輯模式時,ColEdit事件會被引發。
' Save the current cell value before editing. Private Sub DataGrid1_ColEdit(ByVal ColIndex As Integer) ' SaveText is a module-level variable. SaveText = DataGrid1.Text End Sub
藉由查詢CurrentCellModified屬性可以得知目前的Cell是否已被修改,並且也可設定此屬性為False與EditActive為False來取消編輯的運作。CurrentCellVisible屬性在DataGrid物件和Split物件中都有;若目前的Cell在物件中是可看見的話,它會傳回True。假設設定Split物件的CurrentCellVisible屬性為True,Split會捲動到Cell看得見的地方;若設定DataGrid物件的CurrentCellVisible屬性為True,所有Split都會捲動到該Cell看得見之處。當目前的Cell在編輯中,也可讀取和修改Grid物件的SelStart、SelLength及SelText屬性,就像一般的TextBox控制項般。
因為DataGrid控制項總是連接到ADO資料來源,Bookmark屬性可設定和傳回在目前資料錄中的書籤,它通常比Row屬性更為有用。甚至更有趣的,每當使用者移到另一列,原始資料集(underlying Recordset)物件的目前記錄會自動改變來反應新的現行Cell。因此,您可藉著簡單地查詢Recordset物件之Field集合,便可從Recordset物件中取得額外的欄位。以下的程式碼假設DataGrid控制項連接ADO資料來源。
' Display the current product's unit price in Euro currency. ' The RowColChange event fires when a new cell becomes current. Private Sub DataGrid1_RowColChange(LastRow As Variant, _ ByVal LastCol As Integer) ' The DOLLAR_TO_EURO_RATIO variable is defined elsewhere in the module. lblEuroPrice = Adodc1.Recordset("UnitPrice") * DOLLAR_TO_EURO_RATIO End Sub
DataGrid控制項的Split屬性傳回0到Splits.Count-1範圍的整數,指出包含現行儲存格的分割區段。也可以指定一個新的值給此屬性以便移動駐點到另一個分割。當表格被切格成數個區段,DataGrid控制項的少數屬性—例如RecordSelectors 、FirstRow—和目前區塊的屬性是一樣的。換句話說:
' The following statements are equivalent. DataGrid1.RecordSelectors = True DataGrid1.Splits(DataGrid1.Split).RecordSelectors = True
使用其他的Cell
有一些屬性可取得和設定在表格中任一Cell的屬性,但不能總是以直覺方式使用他們。每一column物件包含Text和Value屬性:前者設定或傳回在目前列之下的行之文字,而後者是目前列之下的行之格式化顯示前的實際值。Column物件也包含CellText和CellValue方法,傳回任一列在行中的Cell內容。有幾個方法來取的相對於列的書籤,待會兒會解說。
VisibleRows和VisibleCols是唯讀的屬性,各自傳回可視的列和行的數量。沒有屬性可以直接傳回列和行的總數。可使用ApproxCount屬性,傳回列的近似數;這一值和實際值有差異。為取得行的個數,必須查詢Column集合的Count屬性。
DataGrid物件有兩種方法讓您在控制項中存取任一列的書籤。GetBookmark傳回相對於在目前列的列書籤:GetBookmark(0) 和Bookmark屬性是一樣的;GetBookmark(-1) 是在目前列之前的列書籤;GetBookmark(1) 是目前列之後的列書籤等等。另一個有效的方法,RowBookmark,傳回任一個可見的列書籤:RowBookmark(0) 是第一個可見的列書籤,而RowBookmark(VisibleRows-1) 是最後一個列書籤。
FirstRow屬性傳回第一列的書籤。根據文件,可以指定一個新書籤給此屬性以便捲動Grid內容,但筆者發現當嘗試指定一個值給它時,總會得到一個「無效書籤」錯誤。LeftCol屬性為第一個可視行之索引,所以可以在程式中顯示Grid的左上角,如下的程式碼。
DataGrid1.LeftCol = 0 Adodc1.Recordset.MoveFirst DataGrid1.CurrentCellVisible = True
FirstRow、LeftCol及CurrentCellVisible屬性在Split物件中也有;同樣地,指定值給FirstRow屬性也會發生錯誤。
可使用先前提到的書籤方法所傳回的值作為Column物件CellText、CellValue方法的參數。舉例來說,以下程式碼顯示在目前列和前一列的Total欄位的差異性。
Private Sub DataGrid1_RowColChange(LastRow As Variant, _ ByVal LastCol As Integer) Dim gcol As MSDataGridLib.Column If DataGrid1.Row > 0 Then ' Get a reference to the current column. Set gcol = DataGrid1.Columns("Total") ' Display the difference between the values in the "Total" column ' of the current row and the cell immediately above it. Label1 = gcol.CellValue(DataGrid1.GetBookmark(-1)) - gcol.Value Else Label1 = "(First Row)" End If End Sub
管理Cell的選取
使用者可以藉由按著Shift鍵不放,再按下行的標頭來選擇相鄰的多數個行;也可以藉由按著Ctrl鍵不放,再按下最左邊灰色的行來選擇多數列-即使這些列沒有鄰近亦可-(要能選取多列,尚需要Grid或Split的RecordSelectors屬性被設為True)。SelStartCol和SelEndCol屬性各自設定和傳回第一個和最後一個被選擇的行之索引。藉由設定這些屬性為-1或使用ClearSelCols方法,您可清除行的選取。這些屬性和此方法在Split物件上也可看到。
因為使用者可選擇非鄰近的列, 因此需要靠DataGrid控制項的SelBookmarks集合來得知哪些列被選取,此集合包含所有被選擇的列之書籤。舉例來講,選擇目前列,然後執行以下的敘述。
DataGrid1.SelBookmarks.Add DataGrid1.Bookmark
可以用For Each迴圈處理所選擇的列。譬如,以下的程式碼利用SelChange事件-每次有行或列被選取或被取消選取時皆會引發-來更新Label控制項,此控制項顯示所有被選取列的Total欄位之儲存格的總和。
Private Sub DataGrid1_SelChange(Cancel As Integer) Dim total As Single, bmark As Variant For Each bmark In DataGrid1.SelBookmarks total = total + DataGrid.Columns("Total").CellValue(bmark) Next lblGrandTotal = total End Sub
沒有Method可清除被選擇的列;只有藉由移除SelBookmark集合的所有項目才能達成,程式碼如下所示。
Do While DataGrid1.SelBookmarks.Count DataGrid1.SelBookmarks.Remove 0 Loop
監視編輯動作
DataGrid控制項有豐富的事件集合,幾乎可讓您捕抓使用者的每個動作。幾乎所有事件皆以Beforexxxx和Afterxxxx為形式,Beforexxxx事件接受Cancel參數,可將此設為True來取消運作。我們已經看過ColEdit事件,當在Cell中按下任何鍵來編輯值時會引發此事件。此事件實際上在相對的BeforeColEdit事件之前引發,因此讓您有機會使Cell成為唯讀。
' Refuse to edit a cell in the first column if it already contains a value. Private Sub DataGrid1_BeforeColEdit(ByVal ColIndex As Integer, _ ByVal KeyAscii As Integer, Cancel As Integer) ' note how you can test Null values and empty strings at the same time. If ColIndex = 0 And DataGrid1.Columns(ColIndex).CellValue _ (DataGrid1.Bookmark) & "" <> "" Then Cancel = True End If End Sub
若在BeforeColEdit事件中取消編輯動作,控制項無法復原對此一動作的任何事件,若您已習慣ADO的事件引發方式,則可能會被搞混,因為ADO的事件引發方式是,即使在之前的事件中取消動作,其後的事件依然會被引發。KeyAscii參數值為鍵盤被按下後進入編輯模式的按鍵碼,或按下滑鼠鍵進入編輯模式,此值會是0。因為此參數是以值傳遞,所以不能改變它。然而,這不是一個問題,因為Grid也接受所有的KeyDown、KeyPress及KeyUp事件,可讓您修正包含使用者按下鍵盤的按鍵碼之參數值。
每次在Cell中修正值時,DataGrid控制項會收到Change事件;若編輯動作真的修改了Cell中的值-亦即,沒有用按ESC鍵取消之-此控制項接著也收到BeforeColUpdate和AfterColUpdate事件。
Private Sub DataGrid1_BeforeColUpdate(ByVal ColIndex As Integer, _ OldValue As Variant, Cancel As Integer) ' Trap invalid values here. End Sub
不過注意程序中的警訊。不能使用DataGrid物件或Column物件的Text或Value屬性來存取即將進入的Grid的值,因為在此一事件程序中,這些屬性傳回在Grid中Cell的初始值-和OldValue參數傳回的值是一樣的。當EditActive屬性為True時,DataGrid物件的Text屬性傳回使用者鍵入的字串,但當處理BeforeColUpdate事件時,此一屬性已經再被重設為False了。解決方法是宣告一個表單層次變數,且在Change事件中指定一個值給它。舉例來說,以下的程式碼確認已經被鍵入的值不會重複出現在Recordset的任一其他記錄中。
Dim newCellText As String ' Remember the most recent value entered by the user. Private Sub DataGrid1_Change() newCellText = DataGrid1.Text End Sub ' Check that the user isn't entering a duplicate value for that column. Private Sub DataGrid1_BeforeColUpdate(ByVal ColIndex As Integer, _ OldValue As Variant, Cancel As Integer) Dim rs As ADODB.Recordset, fldName As String ' Retrieve the field name for the current column. fldName = DataGrid1.Columns(ColIndex).DataField ' Search for the new value in the Recordset. Use a clone Recordset ' so that the current bookmark doesn't change. Set rs = Adodc1.Recordset.Clone rs.MoveFirst rs.Find fldName & "='" & newCellValue & "'" ' Cancel the operation if a match has been found. If Not rs.EOF Then Cancel = True End Sub
說明
「警訊」正式來說是一個錯誤,在Microsoft Knowledge Base的Q195983中有說明。然而,在這裡提出的方法比在文章中提到的簡單多了,該文的解決方法用的是Grid的hWndEdito屬性和GetWindowText API函數。
當游標移到另一列時,BeforeUpdate和AfterUpdate這一對事件會引發,此讓您有機會來進行記錄層次的查核,且可可選擇是否拒絕更新。當使用者編輯在行中的值,然後移到下一個或前一個資料列時,底下為其會引發的事件。
KeyDown
KeyPress |
使用者按下按鍵。 |
BeforeColEdit
ColEdit |
Grid進入編輯模式。 |
Change | 現在可用Text屬性獲得最新值。在此,ActiveEdit屬性值變成True。 |
KeyUp | 第一個按鍵被釋放。 |
KeyDown
KeyPress Change KeyUp |
其他按鍵被釋放。 |
其他按鍵被按下 | |
BeforeColUpdate
AfterColUpdat AfterColEdit |
使用者移動游標至其他行。 |
RowColChange | 只有當移動完成時,方會引發此事件。 |
BeforeUpdate
AfterUpdate |
使用者移動游標至其他列。 |
RowColChange | 只有當移動完成時,方會引發此事件。 |
注意
要小心在DataGrid控制項中的事件程序程式碼。首先,少數事件,例如:RowColChange,當Grid被分割成二至多個區域時可能引發許多次,所以應該要避免執行一次以上的相同敘述。然而,當現行記錄以程式轉移到另一不完全看得見的列時,並不會引發RowColChange事件;於此例中,Grid會捲動捲軸以便讓新記錄成為可視的,但並不會引發此事件。這一問題也出現在當使用者藉由ADO物件的資料控制項按鈕來移動游標至無法完全看見的記錄。
執行新增和刪除動作
使用者可以選擇要刪除的列,然後按下Delete鍵刪除這些列。這一動作會引發BeforeDelete事件(在此可以取消刪除命令)和AfterDelete事件,以及BeforeUpdate和AfterUpdate這對事件。舉例來說,可以在BeforeDelete事件中寫程式碼來確認目前的記錄是在主從關係中的主記錄,然後,不是取消此一動作(如底下程式),就是自動刪除相關的記錄。
Private Sub DataGrid1_BeforeDelete(Cancel As Integer) Dim rs As ADODB.Recordset, rsOrderDetails As ADODB.Recordset ' Get a reference to the underlying Recordset Set rs = Adodc1.Recordset ' Use the connection to perform a SELECT command that checks whether ' there is at least one record in the Order Details table that has ' a foreign key that points to the ProductID value of current record. Set rsOrderDetails = rs.ActiveConnection.Execute _ ("Select * FROM [Order Details] WHERE [Order Details].ProductID=" _ & rs("ProductID")) ' If EOF = False, there is a match, so cancel the delete command. If Not rsOrderDetails.EOF Then Cancel = True End Sub
若您取消刪除動作,DataGrid控制項顯示錯誤訊息。可以透過Error事件來忽略它和其他來自控制項的錯誤訊息。
Private Sub DataGrid1_Error(ByVal DataError As Integer, _ Response As Integer) ' DataError = 7011 means "Action canceled" If DataError = 7011 Then MsgBox "Unable to delete this record because there are " _ & "records in the Order Details table that point to it." ' Cancel the standard error processing by setting Response = 0. Response = 0 End If End Sub
在此事件中,DataError參數包含錯誤碼,另一方面Response參數包含1;可設定Response參數為0來避免Grid顯示一般的錯誤訊息,如前例所述。也可藉由DataGrid的ErrorText屬性測試標準錯誤訊息。
若AllowAddNew屬性為True,DataGrid控制項在按鈕下會顯示一個已星號為標記的空白列,而使用者可以進入新列,藉由在此列的任一Cell中鍵入文字。當此動作發生時,控制項會引發BeforeInsert事件,隨後是AfterInsert事件(除非取消此一動作),然後是OnAddNew事件。完整的事件描述如下:
BeforeInsert
AfterInsert OnAddNew |
使用者在最後一列按一下滑鼠。 |
RowColChange | 只有當移動完成時,方引發此事件。 |
BeforeColEdit
ColEdit Change Other Change and Keyxxx events |
使用者按下按鍵。 |
BeforeColUpdat AfterColUpdate | 使用者移動游標到同一列的另一行。 |
RowColChange | 只有當移動完成時,方引發此事件。使用者在同一列的其他Cell中鍵入值。 |
BeforeUpdate AfterUpdate | 使用者移動游標至另一列。 |
RowColChange | 只有當移動完成時,方引發此事件。 |
透過AddNewMode屬性,可以監視目前的狀態,有以下的值可指定:0-dbgNoAddNew (無AddNew命令在進行中)、1-dbgAddNewCurrent(目前在最後一列,但無AddNew命令在進行)、2-dbgAddNewPending(目前在最後一列的下一列,而AddNew命令在處理中)。可以透過使用者或程式碼來初始AddNew命令,以便產生指定值給Text或Value屬性的結果。
捕抓滑鼠事件
DataGrid控制項有所有常見的滑鼠事件,可得知按下滑鼠的座標和控制鍵的狀態等。不幸地,DataGrid控制項無法支援OLE拖曳動作,因此沒有OLExxxx屬性、方法和事件。當以滑鼠運作時,會使用控制項提供的三種方法:RowContaining方法傳回滑鼠指標所落在的可見列;ColContaining方法傳回相對的行數;最後,SplitContaining方法傳回分割數。當滑鼠移出Grid範圍-例如,滑鼠位在記錄的選擇區時-這些方法傳回-1。這有個範例,其以ToolTipText屬性來顯示在滑鼠之下的CellUnderlying值,若行太窄以致於無法顯示較長字串時,這特別有用。
Private Sub DataGrid1_MouseMove(Button As Integer, Shift As Integer, _ X As Single, Y As Single) Dim row As Long, col As Long On Error Resume Next row = DataGrid1.RowContaining(Y) col = DataGrid1.ColContaining(X) If row >= 0 And col >= 0 Then DataGrid1.Tool小密訣Text = DataGrid1.Columns(col).CellValue _ (DataGrid1.RowBookmark(row)) Else DataGrid1.Tool小密訣Text = "" End If End Sub
改變Grid版面
可使用Split和Column集合許多屬性和方法的其中之一來改變DataGrid控制項的版面。例如,以Column.Add方法來新增一行,如下所示:
' Add a Product Name column. (It will become the 4th column.) With DataGrid1.Columns.Add(3) .Caption = "Product Name" .DataField = "ProductName" End With ' You need to rebind the grid after adding a bound column. DataGrid1.ReBind
從版面中移除一行,使用Column.Remove方法:
' Remove the column added by the previous code snippet. DataGrid1.Columns.Remove 3
用Splits.Add方法增加需要的分割區。傳遞給此方法的參數是新的位置(在Grid的最左邊為0)。
' Add a new split to the left of all existing splits. DataGrid1.Splits.Add 0
在建立分割後,必須決定哪些行在Datagrid控制項中是可見的。因為每個新分割會繼承Grid的所有行,從一個Split移除一行也會在其他所有的Split中移除它,在此章前面的 〈修改行外觀〉 一節中有講到。以下的程式碼說明若要刪除不要的行,只需使它們隱藏即可:
' Add a new split to the right of the existing split. With DataGrid1.Splits.Add(1) ' Ensure that the two splits divide the grid's width in half. ' Assumes that the existing split's SizeMode property is 0-dbgScalable. ' (Always set SizeMode before Size!) .SizeMode = dbgScalable .Size = DataGrid1.Splits(0).Size ' This new split can be scrolled independently. .ScrollGroup = DataGrid1.Splits(0).ScrollGroup + 1 ' Hide all the columns except the one labeled "ProductName". For Each gcol In .Columns gcol.Visible = (gcol.Caption = "ProductName") Next End With
處理查閱值
常常從資料庫資料表中取得的值本身是沒有意義的,但是有用的,因為它是外鍵,其會對應到存放真正資料的另一個資料表。譬如,在Nwind資料庫中Products資料表包括SupplierID欄位,此欄位為Suppliers資料表的鍵值,而在Suppliers資料表可得到該特定產品所屬供應商的名稱和地址。當將Products資料表以DataGrdi控制項顯示時,可以在ADO資料感知物件的RecordSource屬性裡用適當的JOIN敘述,以便讓表格自動地顯示正確的供應商名稱(代替其鍵值)。
然而,ADO連接機制提供更好的方法。此技巧是宣告一個StdDataFormat物件,將此物件指定給Column物件的DataFormat屬性,然後使用Format事件來轉換來自資料來源的數值資料為描述性的文字串。以下例子從次資料表中載入所有資料到隱藏的ComboBox控制項中。然後,在StdDataFormat物件的Format事件中使用ComboBox控制項的內容以轉換SupplierID鍵值為供應商的CompanyName欄位。
Dim WithEvents SupplierFormat As StdDataFormat Private Sub Form_Load() ' Load all the values from the Supplier lookup table into the ' hidden cboSuppliers ComboBox control. Dim rs As New ADODB.Recordset rs.Open "Suppliers", Adodc1.Recordset.ActiveConnection Do Until rs.EOF cboSuppliers.AddItem rs("CompanyName") ' The SupplierID value goes into the ItemData property. cboSuppliers.ItemData(cboSuppliers.NewIndex) = rs("SupplierID") rs.MoveNext Loop rs.Close ' Assign the custom format object to the SupplierID column. Set SupplierFormat = New StdDataFormat Set DataGrid1.Columns("SupplierID").DataFormat = SupplierFormat ' Make the row height equal to the ComboBox's height. DataGrid1.RowHeight = cboSuppliers.Height End Sub Private Sub SupplierFormat_Format(ByVal DataValue As _ StdFormat.StdDataValue) Dim i As Long ' Search the key value in the cboSuppliers ComboBox. For i = 0 To cboSuppliers.ListCount - 1 If cboSuppliers.ItemData(i) = DataValue Then DataValue = cboSuppliers.List(i) Exit For End If Next End Sub
使用ComboBox控制項當作查閱資料表內容的儲存場所並不是碰巧的抉擇。實際上在某些特殊情況下,我們甚至可以用ComboBox控制項讓使用者選擇新的SupplierID值。我們必須做的是讓ComboBox控制項顯示在DataGrid控制項前面,完全涵蓋使用者編輯的Cell,而當使用者從清單中選擇一個新值時,更新舊有的SupplierID欄位。對於最明顯的效果,還必須捕抓少數事件以便讓ComboBox控制項總是在正確位置,如 圖15-4 。此技巧的程式碼如下:
Private Sub MoveCombo() ' In case of error, hide the ComboBox. On Error GoTo Error_Handler Dim gcol As MSDataGridLib.Column Set gcol = DataGrid1.Columns(DataGrid1.col) If gcol.Caption = "SupplierID" And DataGrid1.CurrentCellVisible Then ' Move the ComboBox inside the SupplierID column ' if it is the current column and it is visible. cboSuppliers.Move DataGrid1.Left + gcol.Left, _ DataGrid1.Top + DataGrid1.RowTop(DataGrid1.row), gcol.Width cboSuppliers.Zorder cboSuppliers.SetFocus cboSuppliers.Text = gcol.Text Exit Sub End If Error_Handler: ' In all other cases, hide the ComboBox. cboSuppliers.Move _10000 If DataGrid1.Visible Then DataGrid1.SetFocus End Sub Private Sub cboSuppliers_Click() ' Change the value of the underlying grid cell. DataGrid1.Columns("SupplierID").Value = _ cboSuppliers.ItemData(cboSuppliers.ListIndex) End Sub Private Sub DataGrid1_RowColChange(LastRow As Variant, _ ByVal LastCol As Integer) MoveCombo End Sub Private Sub DataGrid1_RowResize(Cancel As Integer) MoveCombo End Sub Private Sub DataGrid1_ColResize(ByVal ColIndex As Integer, _ Cancel As Integer) MoveCombo End Sub Private Sub DataGrid1_Scroll(Cancel As Integer) MoveCombo End Sub Private Sub DataGrid1_SplitChange() MoveCombo End Sub
此段程式碼還需要讓DataGrid控制項的RowHeight屬性等於ComboBox控制項的Height屬性。因為後者在執行階段是唯讀的,所以在Form_Load事件程序中執行下列敘述:
' Have the row height match the ComboBox's height. DataGrid1.RowHeight = cboSuppliers.Height
關於查閱資料表的另一個方法是架構在Column物件的Button屬性和ButtonClick事件上。然而用此方法,若將ListBox(或DataList)控制項顯示在儲存格下方會比蓋過它顯示要好。後者的實作方式與之前的雷同,就留給您練習了。
圖15-4 此範例顯示有著下拉式ComboBox控制項的查閱欄位,及支援splits,排序等命令。 |
資料排序
DataGrid控制項沒提供任何資料排序的函數。然而,HeadClick事件和ADORecordset之Sort屬性只要藉由一些敘述就能做資料排序的工作。
Private Sub DataGrid1_HeadClick(ByVal ColIndex As Integer) ' Sort on the clicked column. Dim rs As ADODB.Recordset Set rs = Adodc1.Recordset If rs.Sort <> DataGrid1.Columns(ColIndex).DataField & " ASC" Then ' Sort in ascending order; this block is executed if the ' data isn't sorted, is sorted on a different field, ' or is sorted in descending order. rs.Sort = DataGrid1.Columns(ColIndex).DataField & " ASC" Else ' Sort in descending order. rs.Sort = DataGrid1.Columns(ColIndex).DataField & " DESC" End If ' No need to refresh the contents of the DataGrid. End Sub
此方法只有一個限制就是若有資料行包含查閱值時是無法運作的。
階層式FlexGrid控制項
階層式FlexGrid控制項是另一個包含在Visual Basic 6的Grid控制項。與DataGrid控制項不同的是,階層式FlexGrid控制項可以合併不同列但包含相同值之相鄰Cell。當指定階層性ADO Recordset給此控制項的DataSource屬性時,真正發揮此控制項的特性,因為它可以正確地顯示多重Band-每個Band是一連串來自階層式資料結構的不同子Recordset的資料行,如圖15-5所示。此控制項唯一嚴重的限制是它是唯讀的-亦即使用者不能直接編輯Cell。
建立階層式FlexGrid控制項最簡單的方式是在DataEnvironment設計師中建立階層式Command物件,使用滑鼠右鍵拖曳Command物件於表單中,然後從快捷選單中選擇階層式FlexGrid選項。此一動作增加對控制項型態的需求參考且連接新建立的階層式FlexGrid控制項到Command物件上。這一節的所有例子-都在光碟片上-皆是探討階層式Recordset,其來自Biblio.mdb資料庫中Authors、Title和Titles資料表間的關係。
說明
階層式FlexGrid控制項和舊版的FlexGrid控制項(仍然包含在Visual Basic 6中,但無法支援新的ADO資料控制項和資料來源)相容。幸虧此相容性,階層式FlexGrid控制項才得以取代FlexGrid控制項。兩者間微小的差異將在下列章節中討論。
階層式FlexGrid控制項包含於MSHFLXGD.OCX檔案中,所以若應用程式有用到此控制項,此檔案必須隨之散發。
圖15-5 階層式FlexGrid控制項顯示三層的階層式ADO Recordset(包含Biblio.mdb資料庫的Authors、Title、及Titles資料表) |
設定設計階段屬性
在建立有著資料繫結的階層式FlexGrid控制項後,在此控制項上按下滑鼠右鍵,選擇擷取結構選項可將行標題填滿Grid,每一標題指向資料來源的不同欄位。可惜,此Grid沒有Edit命令,因此在設計階段無法使用滑鼠來修正資料行版面和寬度。不像DataGrid控制項,階層式FlexGrid控制項沒有物件模型。
一般頁籤
如圖15-6所示的一般頁籤可設定控制項的Rows和Cols屬性,您應該預期這些屬性會決定Grid的列數和行數。然而,這些屬性只限於非連接模式下影響此控制項的外觀-即是DataSource屬性未指向ADO資料來源時。至於其他方面,表格的大小是依據資料來源的記錄及欄位數量而定。FixedRows和FixedCols屬性決定在表格上界和左界的固定列和行的顯示數量。若AllowBigSelection屬性為True,按下列標頭或行標頭會選取整行或整列。
Highlight屬性決定所選擇儲存格的外觀,其值可能為:0-flexHighlightNever、1-flexHighlightAlways (預設值,被選擇的儲存格總是顯現的)及2-flexHighlightWithFocus (只有當控制項擁有駐點時,被選擇的儲存格才會顯現)。FocusRect屬性決定現行儲存格的邊框為何:0-flexFocusNone (無框線), 1-flexFocusLight (預設值), 或2-flexFocusHeavy。
BandDisplay屬性決定Band在控制項的顯示方式,其值可為0-flexBandDisplayHorizontal (預設值,所有對應到一筆記錄的Band顯示在同一列上)或1-flexBandDisplayVertical (每個Band顯示於不同的列上)。一般情況下,設定表格Text屬性或其他Cell-格式化屬性只影響現行儲存格;可以改變此一預設行為,只要將FillStyle屬性從0-flexFillSingle改為1-flexFillRepeat,在這種情況下所有選擇的儲存格都將會被影響。SelectionMode屬性決定是否可以選擇任一儲存格(0-flexSelectionFree, 預設值)或強迫選擇整列(1flexSelectionByRow)或整行(2-flexSelectionByColumn)。
AllowUserResizing屬性決定使用者可否用滑鼠改變行或列大小,其值有:0-flexResizeNone (不允許改變大小)、1-flexResizeColumns、2-flexResizeRows, or 3-flexResizeBoth (預設值)。若此屬性設為2-flexResizeRows或3-flexResizeBoth,則可用RowSizingMode屬性限制其影響,其值可以為:0-flexRowSizeIndividual (只有改變大小的列被影響,為預設值) 或1-flexRowSizeAll (所有列都改變大小)。
圖15-6 階層式FlexGrid控制項屬性頁的一般頁籤。 |
Band頁籤
FlexGrid控制項屬性頁中最重要的頁籤為Band頁籤,因為在此可決定Grid要出現主Recordsets和子Recordsets的哪些欄位。基本上,應該不要顯示對使用者無意義的數值鍵值和重複出現的外鍵值。例如範例中,筆者在Band 1(此band立即指向Title Author資料表)隱藏Au_ID和ISBN欄位,因為Au_ID對使用者無意義,而ISBN欄位已經在Band 2中出現(此Band指向Titles資料表)。因為Band 1的所有欄位都是不可視的,表格實際上只顯示兩個Band。也可以改變任一可視欄位的行標題,如圖15-7。
Band頁籤也允許設定其他的Band特性。在格線樣式欄位中,可選擇現行Band及下一Band間的線條樣式。此值對應到GridLinesBand屬性且可為下列值之一:0-flexGridNone、1-flexGridFlat (預設值,顏色已被GridColor屬性所決定)、2-flexGridInset、3-flexGridRaised、4-flexGridDashes或5-flexGridDots。
在文字樣式下拉選單中,可選擇3-D效果用來顯示band中文字。此對應到TextStyleBand屬性,且為以下其中一值: 0-flexTextFlat (預設值)、 1-flexTextRaised、2-flexTextInset、3-flexTextRaisedLight或4-flexTextInsetLight。設定為1或2對於較大的粗體字是較好的,而3或4適合小字型。TextStyleHeader屬性值也一樣,但影響行標題上的文字型態。
BandIndent屬性決定Band縮排的行數;此屬性只有在BandDisplay屬性設為1-flexBandDisplayVertical時才有作用。BandExpandable布林屬性表示此Band為展開或收起;加號或星號會在Band的第一行出現,除非此Band在此列是最後一個。此頁籤最後一個屬性是ColumnHeaders,它決定了是否Grid要在Band上方顯示行標頭。
圖15-7 Band頁籤可決定每個Band中哪些欄位要顯示,與其標題為何。 |
其他頁籤
樣式頁籤允許您設定其他影響Grid外觀的屬性。GridLinesFixed屬性決定Grid格線的樣式(值與GridLinesBand屬性相同)。TextStyleFixed屬性決定固定列和行的文字3-D樣式(值與TextStyleBand屬性值相同)。
MergeCells屬性決定有相似值的鄰近儲存格如何合併;此屬性只有在Grid內容是手動填入時才有用,而當控制項是連接到階層式ADO Recordset時則沒有效果(請參閱Visual Basic線上文件查閱進一步的訊息。)
RowHeightMin屬性設定列的最小高度(單位為twip)。GridLinesUnpopulated屬性則決定不含值的儲存格樣式。當文字超過儲存格的長度時,若您想讓儲存格中的文字換行則WordWrap屬性應該設為True。
階層式FlexGrid控制項有許多顏色和字型的屬性,可用來指定顏色和字型。請看Visual Basic文件有這些屬性的詳細資料。
執行階段運作
階層式FlexGrid控制項有將近160個屬性,若要完整地解說可能得再寫另一本書了。不過大部分的屬性影響控制項的少許外觀,沒什麼值得大書特寫的。實際上所需要了解的應是此控制項最重要的屬性、方法和事件。
在現行儲存格的運作
階層式FlexGrid控制項最重要的執行階段屬性是Row、Col及Text,這些屬性設定和傳回現行儲存格的座標和內容。記得此控制項是唯讀的:可用程式改變Grid任一儲存格的內容,不過新值不會存入資料庫。也要記得表格會自動地合併有相同值的儲存格。如
圖15-4 中的Grid,在行1和列2至5的儲存格有相同的值,可以藉由已合併的任一儲存格的Text屬性來改變其值。此一控制項有許多唯讀屬性,這些屬性傳回有關現行儲存格的資訊。例如,藉查詢BandLevel屬性,可以發現現行儲存格屬於哪一個Band,而查詢Grid的CellType屬性可以知道現行儲存格的型態,此屬性會傳回下列的值:0-flexCellTypeStandard、1-flexCellTypeFixed、2-flexCellTypeHeader、3-flexCellTypeIndent、或4-flexCellTypeUnpopulated。
不像DataGrid控制項,階層式FlexGrid控制項允許完全地決定現行儲存格的外觀,透過如CellBackColor、CellForeColor、CellFontName、CellFontSize、CellFontBold、CellFontItalic、CellFontUnderline、CellFontStrikeThrough和CellFontWidth等屬性。例如,以下的程式碼讓使用者雙按任一儲存格選擇之並改變其背景為紅色。
Private Sub MSHFlexGrid1_DblClick() If MSHFlexGrid1.CellBackColor = vbWindowBackground Then ' Highlight a cell with white text on red background. MSHFlexGrid1.CellBackColor = vbRed MSHFlexGrid1.CellForeColor = vbWhite Else ' Restore default colors. MSHFlexGrid1.CellBackColor = vbWindowBackground MSHFlexGrid1.CellForeColor = vbWindowText End If End Sub
CellTextStyle屬性決定在現行儲存格內文字的3-D效果。CellAlignment屬性設定和傳回現行儲存格內文字的對齊方式;它有以下這幾種值:1-flexAlignLeftCenter、2-flexAlignLeftBottom、3flexAlignCenterTop、4flexAlignCenterCenter、5-flexAlignCenterBottom、6flexAlignRightTop, 7flexAlignRightCenter、8flexAlignRightBottom、9-flexAlignGeneral (內定值,文字靠左,數字靠右)。
也可以顯示圖片於現行儲存格中,只要指定適合的值給CellPicture屬性,且透過CellPictureAlignment屬性指定圖片的對齊方式即可。例如,可在左上角顯示一個文字字串且在右下角顯示圖片。
MSHFlexGrid1.CellAlignment = flexAlignLeftTop MSHFlexGrid1.Text = "This is an arrow" MSHFlexGrid1.CellPictureAlignment = flexAlignRightBottom ' You might need to edit the path to this icon file. Set MSHFlexGrid1.CellPicture = LoadPicture( _ "C:\Microsoft Visual Studio\Graphics\Icons\Arrows\Arw02rt.ico")
存取其他儲存格
FillStyle屬性若被設為1-flexFillRepeat,之前所提的大部分屬性將會影響所有選取的儲存格。這包括CellPicture、CellPictureAlignment及所有的CellFontxxxx屬性。因此有方法改變一群儲存格的設定,即是藉由指定值給原本指定給單一儲存格的相同屬性。提醒您:雖然可指定值給Text屬性以便讓所選擇的儲存格有相同的字串,但有某些情況此動作會產生錯誤「IMSHFlexGrid的Text法失敗」。基於此理由,當一個以上的儲存格被選取時,不該指定值給Text屬性,或至少該以On Error敘述來進行保護措施。
為善用指定值給單一屬性而影響多重儲存格的特性,必須學著如何使用RowSel和ColSel屬性來得知目前所選擇區域的座標。這些屬性傳回在矩形選擇區中角落的儲存格的行和列。在相對角落的儲存格是活動中的儲存格,且位置記錄於Row和Col屬性。這表示若要得知選取區內所有的儲存格,必須撰寫下列程式碼:
' Evaluate the sum of all the cells in the current selection. Dim total As Double, r As Long, c As Long Dim rowMin As Long, rowMax As Long Dim colMin As Long, colMax As Long ' Determine the minimum and maximum row and column. If MSHFlexGrid1.Row < MSHFlexGrid1.RowSel Then rowMin = MSHFlexGrid1.Row rowMax = MSHFlexGrid1.RowSel Else rowMin = MSHFlexGrid1.RowSel rowMax = MSHFlexGrid1.Row End If If MSHFlexGrid1.Col < MSHFlexGrid1.ColSel Then colMin = MSHFlexGrid1.Col colMax = MSHFlexGrid1.ColSel Else colMin = MSHFlexGrid1.ColSel colMax = MSHFlexGrid1.Col End If ' Loop on all the selected cells. On Error Resume Next For r = rowMin To rowMax For c = colMin To colMax total = total + CDbl(MSHFlexGrid1.TextMatrix(r, c)) Next Next
此程式使用TextMatrix屬性,此屬性會傳回Grid中任一儲存格的內容。即使儲存格橫跨多個列或行,此程式仍是正確的。因為在此情況下,TextMatrix屬性只傳回合併儲存格最左上角座標的非空內容,所以決不會重複得到相同的值。
Clip屬性提供一個有效的方法來指定值給目前被選擇的儲存格。首先,準備一個以Tab分隔的字串,以vbCr字元將各列分離,以vbTab字元將行分割。然後調整RowSel和ColSel屬性好選取一個範圍的儲存格,最後將此字串指定給Clip屬性。
Dim clipString As String clipString = "TopLeft" & vbTab & "TopRight" & vbCr & "BottomLeft" _ & vbTab & "BottomRight" & vbCr ' Range must be 2 rows by 2 columns to match the clipString. MSHFlexGrid1.RowSel = MSHFlexGrid1.Row + 1 MSHFlexGrid1.RowCol = MSHFlexGrid1.Col + 1 MSHFlexGrid1.Clip = clipString
根據說明文件,此屬性應該也會用Tab分隔字串傳回目前範圍的內容;可惜這有些錯誤,因為此屬性總是傳回空字串。Clip屬性在MSFlexGrid控制項中很正常,所以當轉換舊版Visual Basic 5.0程式到Visual Basic 6.0要小心。可使用下列函式來模擬Clip屬性,直到此錯誤被修正。
' Return the Clip property for an MSHFlexGrid control. Function MSHFlexGrid_Clip(FlexGrid As MSHFlexGrid) As String Dim r As Long, c As Long, result As String Dim rowMin As Long, rowMax As Long Dim colMin As Long, colMax As Long ' Find minimum and maximum row and column in selected range. If FlexGrid.Row < FlexGrid.RowSel Then rowMin = FlexGrid.Row rowMax = FlexGrid.RowSel Else rowMin = FlexGrid.RowSel rowMax = FlexGrid.Row End If If FlexGrid.Col < FlexGrid.ColSel Then colMin = FlexGrid.Col colMax = FlexGrid.ColSel Else colMin = FlexGrid.ColSel colMax = FlexGrid.Col End If ' Build the clip string. For r = rowMin To rowMax For c = colMin To colMax result = result & FlexGrid.TextMatrix(r, c) If c <> colMax Then result = result & vbTab Next result = result & vbCr Next MSHFlexGrid_Clip = result End Function
Clip屬性可解決階層式FlexGrid控制項中一個已知的問題:當使用資料連結模式時,此控制項無法顯示超過2048列。當連接Grid到一個超過2048筆記錄的資料來源時,雖然Rows屬性包含正確的記錄數目,但只有前2048筆記錄會顯示在Grid中。為了讓資料來源中的所有記錄都能顯示,可用ADORecordset的GetString屬性來獲得所有記錄,然後指定此結果給Grid的Clip屬性。對此一訊息的額外資訊,請參閱Microsoft Knowledge Base的Q194653文章。
改變行特性
有些屬性會影響行的特性。ColAlignment屬性決定在每一行中標準儲存格內的值該如何顯示:
' Align the contents of all standard cells in column 2 to center and bottom. ' Column indexes are zero-based. MSHFlexGrid1.ColAlignment(2) = flexAlignCenterBottom
ColAlignmentFixed屬性作用相同,不過它影響的示固定列之儲存格。
' Align column headers to left and center. MSHFlexGrid1.ColAlignmentFixed(2) = flexAlignLeftCenter
ColWordWrapOption屬性若設為True,則會讓每一行標準儲存格內的文字自動換行;另一方面,ColWordWrapOptionFixed屬性則影響行標頭儲存格的換行特性。
' Enable word wrapping in all cells in column 5. MSHFlexGrid1.ColWordWrapOption(4) = True MSHFlexGrid1.ColWordWrapOptionFixed(4) = True
階層式FlexGrid控制項提供一個非標準的方法來設定行或列的標頭。可用TextMatrix屬性分別設定它們,但還可藉由FormatString屬性很簡單的一次指定全部。此情況之下,必須將標題以「|」連結後傳遞。可在行標頭前面加上特殊字元以便影響其對齊方式(<置左、^置中、>置右);也可以加一段以分號分離的字串,包含標頭的所有字串。以下舉例:
' Display year numbers in column headers and month names in row headers. MSHFlexGrid1.FormatString = "Sales|> 1998|> 1999|> 2000" _ & ";Sales|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec"
每行標題的寬度間接影響該行的寬度。若要作精確的設定,請使用ColWidth屬性。這一屬性在實作上有錯誤,當用在固定行時,會讓階層式FlexGrid控制項忽略格式化文字。不過對於一般FlexGrid控制項則都還好(額外的資訊,請看Microsoft Knowledge Base Q197362文章)。
使表格成為可編修的
雖然階層式FlexGrid控制項原本是唯讀的,但若要增加基本的編修機制並不需花費太多的力氣。如您所預期的,這一技巧是放置Text控制項在現行儲存格上,以便使它好似實屬於表格的一部份。需要捕抓一些事件來使TextBox控制項與表格保持同步,但整體來說,這不需要太多的程式碼。
要實作此技巧,加一個TextBox控制項於表單上,然後設定其Visible屬性為False,MultiLine屬性為True,且BorderStyle屬性為0-None。下列函式讓此幽靈TextBox(稱為txtCellEditor)會如所需的顯示或不顯示。
' These variables keep track of the cell that was active ' when edit mode was entered. Dim cellRow As Long, cellCol As Long Sub ShowCellEditor() With MSHFlexGrid1 ' Cancel range selection, if any. .RowSel = .Row .ColSel = .Col ' Move the cell editor into place by making it one pixel smaller ' than the current cell. txtCellEditor.Move .Left + .CellLeft, .Top + .CellTop, _ .CellWidth - ScaleX(1, vbPixels, vbTwips), _ .CellHeight - ScaleY(1, vbPixels, vbTwips) ' Transfer the contents of the current cell into the TextBox. txtCellEditor.Text = .Text ' Move the TextBox in front of the grid. txtCellEditor.Visible = True txtCellEditor.ZOrder txtCellEditor.SetFocus ' Remember current coordinates for later. cellRow = .Row cellCol = .Col End With End Sub Sub HideCellEditor(Optional Cancel As Boolean) ' Hide the TextBox control if necessary. If txtCellEditor.Visible Then ' If the operation hasn't been canceled, transfer the contents ' of the TextBox into the cell that was active. If Not Cancel Then MSHFlexGrid1.TextMatrix(cellRow, cellCol) = txtCellEditor.Text End If txtCellEditor.Visible = False End If End Sub
ShowCellEditor函式能夠定位TextBox,這有賴Grid的CellLeft、CellTop、CellWidth及CellHeight屬性。下一步是決定何時啟動Cell的編輯。在範例中,當表格被連按兩下或當Grid擁有輸入駐點時,使用者按下一個字母和數字的鍵的話,都會進入編輯模式。
Private Sub MSHFlexGrid1_DblClick() ShowCellEditor End Sub Private Sub MSHFlexGrid1_KeyPress(KeyAscii As Integer) ShowCellEditor ' If it's an alphanumeric key, it is passed to the TextBox. If KeyAscii >= 32 Then txtCellEditor.Text = Chr$(KeyAscii) txtCellEditor.SelStart = 1 End If End Sub
當TextBox控制項喪失駐點(例如當使用者在非Grid區按一下),或當Enter鍵或Esc鍵被按下時,會中斷編輯模式。
Private Sub txtCellEditor_LostFocus() HideCellEditor End Sub Private Sub txtCellEditor_KeyPress(KeyAscii As Integer) Select Case KeyAscii Case 13 HideCellEditor Case 27 HideCellEditor True ' Also cancel the edit. End Select End Sub
此一簡單的例子只修正階層式FlexGrid控制項的內容,而沒有影響既有的ADO階層式Recordset。更新既有的ADO階層式Recordset是件較複雜的工作,但Grid提供決定哪一筆記錄的哪些欄位要修改所需要的屬性。
DataReport設計師
Visual Basic 6是第一個完整地把報表產生器納入整合環境的版本。比起Crystal報表,新的報表設計師是較容易使用的,尤其針對簡易單純的報表而言。但他仍缺乏一些特性以致於在複雜報表上仍無法取代Crystal報表或其他協力廠商的報表產生器。所以,包裝上仍有Crystal報表,只是您必須手動安裝他。
在使用DataReport設計師前,您必須把他加入整合環境。方法為點選專案選單的設定引用元件選項,切換到設計頁籤,然後把Data Report打勾。接著,您可以建立一個新的資料庫專案或利用Visual Basic產生一個DataReport設計師的實體。
DataReport設計師只能以連結方式工作,透過這種方式可以自動地帶出資料並送至印表機或僅是單純地顯示於預覽視窗。此外也能將資料匯出成文字檔或HTML檔以及其他格式的檔案。DataReport設計師包含一整套的控制項,可讓您放置於其上,就像放置於表單或其他設計師般。這些控制項包括了lines、shapes、images和公式欄位(可用來建立加總欄位等)。另一項特點為它具有非同步列印的能力,如此一來當報表列印時,使用者還能進行其他作業。
設計階段的作業
最簡單使用DataReport設計師建立報表的方式為與DataEvnironment設計師進行連結。DataReport設計師支援DataEnvironment Command物件(包含階層式Command物件)的拖曳功能。唯一限制是在每個巢狀層次上只能有一個子Recordset。在本章的所有範例中,筆者將使用建築在Nwind.mdb的Orders與Order Details上的階層式Command物件。如同往例,完成的範例程式均在隨書光碟中。
連結至Command物件
下列為建立一個架構於簡單的階層式Command物件的報表的方式:
在討論別的主題前,還有兩個關於控制項位置的觀念需要闡述。第一,您可以放置任一個控制項至對應到Command物件的區段,包括較深層的任一區段。例如,您可自Orders Command中放置OrderID欄位到Orders區段與Order_Details區段。然而,卻無法把UnitPrice從Order_Details區段移到Order區段。第二,無法自DataEvnironment放置二進位的欄位或包含圖片的欄位到DataReport設計師中;當如此做時,Visual Basic並不會產生錯誤,但會再執行時產生包含無意義字元的RptTextBox控制項。
圖15-8 設計階段的DataReport設計師,當在控制項上按下滑鼠右鍵時會開啟快捷功能表 |
設定控制項屬性
這些放置到DataReport上的控制項很類似放置在表單上的標準控制項,但他們是屬於不同的控制項函式庫。事實上,標準內建的控制項是無法放置到DataReport設計師中,而屬於DataReport的控制項也無法放置到表單或其他設計師上。但您可以移動DataReport控制項以及將其排列成您要的,就像任一個正常的控制項般。然而,卻無法使用在格式選單內的選項,只不過您可以在控制項上按下滑鼠右鍵,使用快捷功能表內的選項,如圖15-8所示。
DataReport控制項對F4按鍵的反應與其他控制項相同,都會顯示屬性視窗。因為RptLable與RptTextBox控制項與其他標準的控制項是很相似的,所以在屬性視窗當中所見到的屬性,您應該大多熟悉了。例如,改變txtOrderDate與txtShippedDate控制項的DataFormat屬性使之以長日期格式顯示其值。或改變txtOrderID控制項的BackStyle屬性為1-rptBkOpaque和BackColor屬性為灰色(&HE0E0E0)使得order鍵值在報表中是高亮度的。RptLable控制項並未包含Dataxxxx屬性;他們只是裝飾用的控制項,用來顯示固定的字串罷了。
尚未討論到的唯一一個屬性為CanGrow,RptLabel與RptTextBox皆有此屬性。若此屬性值為真,當控制項的內容超過其寬度時會自動延展。預設值為偽,表示超過的字串會被切掉。
增加控制項
可以自工具列及DataEnvironment設計師中增加新控制項至DataReport。事實上,工具列包含DataReport頁籤(包含所有在MSDataReportLib函式庫的控制項)。除了RptLable與RptTextBox控制項外,這函式庫還包含下列項目:
例如,放置一條水平線到Orders_Footer群組,如圖15-8所示。此控制項繪製一條線來分隔在詳細資料的每一群。藉由BorderStyle屬性,還可繪製許多類型的點線。
顯示計算欄位
有兩種顯示計算欄位的方式。第一種方式較適合根據同筆資料的其他值得來的計算欄位,其需要修改SELECT語句以便將計算欄位包含在要被擷取的欄位中。在Orders範例中,便需要將Order Details Command物件改成下列的SELECT查詢語句:
SELECT OrderID, ProductID, UnitPrice, Quantity, Discount, ((UnitPrice*Quantity)*(1-Discount)) AS Total FROM [Order Details]
然後需要在詳細資料區段增加一個Total欄位用來顯示Order Details資料表中每筆資料的總價。記得將此欄位置右對齊以及考慮小數位數。這種展現計算欄位的方式是變化多端的,因為可以使用SQL所提供的所有函數。但他只能針對一筆筆資料基礎下來運作。
另一種藉用SQL優點的方法為在SELECT語句中使用JOIN關鍵字來擷取其他資料表的資料。例如,您可以在Order Details Command物件中使用下列的SELECT語句,此用來將Order Details資料表中的ProductID轉換成產品名稱(來自Products資料表):
SELECT [Order Details].OrderID, [Order Details].ProductID, [Order Details].UnitPrice, [Order Details].Quantity, [Order Details].Discount, (([Order Details].UnitPrice*[Order Details].Quantity)*(1-[Order Details].Discount)) AS Total, Products.ProductName FROM [Order Details] INNER JOIN Products ON [Order Details].ProductID = Products.ProductID
您可以使用相同的技術來顯示顧客名稱(在Orders_Header區段)。然而,範例程式卻用不同的技術得到相同的結果。關於這個,在本章末的〈加入動態的格式與查閱欄位〉一節中再做解釋。
另一個增加計算欄位的技術為透過RptFunction控制項,這種技術較適合總和欄位。例如,增加一個欄位用來計算每筆訂單的總價。這需要計算在Order_Details Command中的Total欄位總和。為了達到這樣的結果,必須放置一個RptFunction控制項到Orders_Footer區段(這是在資料被加總顯示區段後的第一個頁尾)。然後設定此控制項的DataMember屬性為Order_Details,DataFiled欄位為Total,FunctionType值為0-rptFuncSum,以及DataFormat屬性為貨幣。使用相同的方式,可以增加另一個總和欄位用來計算每筆訂單中不同產品的總數,只要設定DataField為ProductID與FunctionType為4-rptFuncRCnt。
並不一定要將RptFunction控制項放到緊接著資料所在區段的頁尾。例如,為了計算Order_Details Command內Total欄位的總和,可把RptFunction控制項加到報表頁尾區段,也可加入另一個RptFunction控制項去計算Orders區段的Freight欄位總和。絕大部分,只需要設定這些控制項的DataMember屬性為正確的Command物件即可。不幸地,並無法把RptFunction控制項放到頁面頁尾區段,所以無法在每頁末端有該頁總和。
感謝DataEnvironment設計師的能力,在製作需要分群資料的報表上並沒什麼特殊的地方。例如,為了依照國別來顯示客戶的資料,您必須做的是建議一個連結到Customers資料表的Command物件,開啟屬性頁並切換到群組頁籤,然後依照Country欄位來分群。這個動作會建立有著兩個資料夾的新Command物件。請把DataReport設計師的DataMember屬性設定為這個Command,然後執行擷取結構動作讓設計師自動地產生需要的區段。光碟內的範例程式包含了使用此技術的報表。
頁尾與換頁的管理
在頁首或頁尾區段可放置控制項,一般來說都是用來顯示目前頁碼,總頁數,印表日期等。要達到這些結果,請在感興趣的區段按下滑鼠右鍵,點選插入控制項選項,然後自快捷功能表中選擇您要顯示的資訊即可。
藉由這種方式產生出來的控制項為RptLable,其Caption屬性會包含特殊字元。
表15-1 列舉出當所有具意義的特殊字元。您也可以自行放置控制項,然後對此控制項設定適當的Caption屬性-例如,第 %p/%P頁會顯示目前的頁碼以及總頁數。在圖15-9中,會看到報表底線附近的區域顯示出頁尾,加總欄位,以及其他點綴用的資料。
圖15-9 執行階段的DataReport設計師;視窗內的控制元件可讓您列印報表,匯出資料至檔案,以其瀏覽其他頁 |
所有的Section物件皆含有兩個影響換頁的屬性。ForcePageBreak屬性用來決定是要從區段的前或後開始新的一頁,其值有下列:0-rptPageBreakNone(預設值),1-rptPageBreakBefort(在列印該頁前先換行)、2-rptPageBreakAfter(在列印該頁完畢時換行)或3-rptPageBreakBeforeAndAfter(表示在區段前後皆可換行)。
其他會影響加到報表的換行的屬性是KeepTogether布林形態的屬性。若此屬性值為真,DataReport設計師會自動在區段前插入一個換行資訊,假如此區段的內容太多以致於跳到下一頁的話。報表與頁首尾區段皆有這兩個屬性,但在此是忽略他們的。
表15-1 可被RptLable的Caption屬性所接受的特殊字元 |
符號 | 說明 |
---|---|
%d | 目前日期(短格式) |
%p | 目前頁碼 |
%D | 目前日期(長格式) |
%P | 總頁數 |
%t | 目前時間(短格式) |
%i | 報表標題 |
%T | 目前時間(長格式) |
%% | 百分比符號 |
其他報表屬性
DataReport設計師包含許多設計階段屬性,可透過屬性視窗加以修改。這大部分的屬性在表單中都有-例如:Caption、Font、WindowState與ShowInTaskbar-因此,您應該知道如何使用這些屬性了。在這當中,有些(如:Caption與BorderStyle)僅會影響預覽視窗,有些(如:Font)則攸關實體報表
DataReport還有些屬性是特有的。LeftMargin、RightMargin、TopMargin與BottomMargin屬性關係到報表的四邊寬度,而ReportWidth則為報表的寬度。GridX與GridY屬性關係到設計階段格點(執行階段是看不見的)的間距。上述所言的單位皆為twips。DataReport設計師還有個可自訂的屬性為Title,其可包含前小節所提到的%等特殊字元,也可用在對話方塊上頭。
注意
可以藉由把MDIChild屬性設成真,而將DataReport視窗設定為MDI子視窗。不過有時將DataReport視窗自MDI應用程式的WindowList選單中移出時,可能會出差錯。詳細資料,請參閱微軟知識庫(Knowledge Base)第Q195972篇。
執行階段作業
當您藉由放置控制項至DataReport設計師而做出完美報表時,別忘了這是因為物件包含了屬性、方法與事件,而這些特性皆可在執行時間透過程式碼來加以控制。程式碼可放在設計師外頭-例如放在開始進行列印的表單中-或放在DataReport自身模組內。後者可建立複雜的報表,也因為將程式放在設計師模組的關係,可容易地於其他專案再次利用。
列印報表
最容易列印報表的方式為讓使用者按下DataReport預覽視窗左上角的按鈕,以便開始列印程序。使用者可自已安裝的印表機中挑選一台來列印,並選擇列印範圍與列印份數。甚至可暫時輸出到某個檔案中,往後再做真正的列印。要開始進行列印程序,只需要藉由Show方法或將DataReport設計師設定成啟動物件以來顯示DataReport視窗。還可用一些屬性來改變預覽視窗的預設外觀呢!
' Display the DataReport in a modal maximized window. DataReport1.WindowState = vbMaximized DataReport1.Show vbModal
若透過程式碼來開始列印程序,可以做些列印的調整。DataReport設計師的PrintReport方法可接受參數與傳回值,如下:
Cookie = PrintReport([ShowDialog], [Range], [PageFrom], [PageTo])
ShowDialog是布林值,用來決定設計師是否顯示列印對話視窗,Range可有下列的值:0-rptRangeAllPage、1-rptRangeFromTo。若要列印某範圍的頁數,還應該設定PageFrom與PageTo參數為起始頁碼與終止頁碼。PrintReport方法會進行非同步列印程序並傳回一個值,用來表示此列印程序,底下是範例:
' Print the first 10 pages of the report without showing a dialog. Dim Cookie As Long Cookie = DataReport1.PrintReport(False, rptRangeFromTo, 1, 10)
非同步作業的好處
產生報表包含三個步驟:查詢;暫存檔的產生;真正的列印、預覽或匯出。前兩種是同步作業;第三種為非同步作業。當DataReport設計師進行非同步作業時,它會約每隔一秒規律地引發ProcessingTimeOut事件。您可以追蹤這個事件以便讓使用者取消較長時間的作業,透過類似下列的程式:
Private Sub DataReport_ProcessingTimeout(ByVal Seconds As Long, _ Cancel As Boolean,ByVal JobType As MSDataReportLib.AsyncTypeConstants,_ ByVal Cookie As Long) ' Display a message every 20 seconds. Const TIMEOUT = 20 ' The value of Seconds when we displayed the last message. Static LastMessageSecs As Long ' Reset LastMessage if a new print operation is in progress. If Seconds < LastMessageSecs Then LastMessageSecs = 0 ElseIf LastMessageSecs + TIMEOUT <= Seconds Then ' A new timeout interval has elapsed. LastMessageSecs = Seconds ' Ask the user whether the operation should be canceled. If MsgBox("This operation has been started " & Seconds _ & " seconds ago." & vbCr & "Do you want to cancel it?", _ vbYesNo + vbExclamation) = vbYes Then Cancel = True End If End If End Sub
JobType參數為作業的種類,可能為下列值:0-rptAsyncPreview、1-rptAsyncPrint、2-rptAsyncExport。Cookie標記出某個作業,且對應到由PrintReport或ExportReport方法所傳回的值(為Long型態)。
如果只是要顯示進度狀態而不要取消某個非同步作業時,可使用AsyncProgress事件,其被引發的時機為每次有新的一頁被送往印表機或被匯出至檔案時:
Private Sub DataReport_AsyncProgress(ByVal JobType As MSDataReportLib.AsyncTypeConstants, ByVal Cookie As Long, _ ByVal PageCompleted As Long, ByVal TotalPages As Long) ' Display the progress in a Label control on the main form. frmMain.lblStatus = "Printing page " & PageCompleted _ & " of " & TotalPages End Sub
若DataReport設計師由於錯誤而無法繼續列印時,其會引發Error事件。在此事件中,便可以了解哪個程序出了差錯,並可藉由把ShowError參數設成偽來消除標準錯誤訊息:
Private Sub DataReport_Error(ByVal JobType As MSDataReportLib.AsyncTypeConstants, ByVal Cookie As Long, ByVal ErrObj As MSDataReportLib.RptError, ShowError As Boolean) ' Display your own custom error message box. If JobType = rptAsyncPrint Or JobType = rptAsyncExport Then MsgBox "Error #" & ErrObj.ErrorNumber & vbCr _ & ErrObj.Description, vbCritical ShowError = False End If End Sub
匯出報表
使用者可匯出報表,只需要點選在DataReport預覽視窗上左側算來第二個按鈕即可。在所顯示的對話方塊中,其必須選擇一個檔名、一個檔案類型與範圍頁數,如同圖15-10所示。DataReport設計師支援四種匯出格式:HTML Text、Unicode、HTML與Unicode Text。注意對話方塊並未顯示總頁數;此值取決於匯出格式且通常與預覽中的頁數不相符合(取決於視窗內的字型)。也要注意匯出的報表不包括RptImage與RptShape等圖形。水平線在HTML報表是可被接受的,而在文字報表內,則以一連串的連字線表示之。表15-2列出註標、符號常數、與字串值等可用來表示四種預先訂好的匯出格式。
圖15-10 匯出對話方塊讓您用四種格式的某一種匯出報表 |
ExportReport方法允許您用程式來匯出報表,看看下列的格式:
Cookie = ExportReport([FormatIndexOrKey], [FileName], [Overwrite], [ShowDialog], [Range], [PageFrom], [PageTo])
FormatIndexOrKey為索引或索引鍵,指出某個預定好的匯出格式,FileName為輸出檔案名,Overwrite為布林值,決定能否覆蓋已存在的檔案(預設為真),ShowDialog為布林值表示標準匯出格式是否要顯示。其餘參數與PrintReport方法相同。ExportReport方法傳回一個Long型態的值,用來在ProcessingTimeout、AsyncProgress或Error事件中表示此特定作業。FormatOrIndexKey值可在表15-2前三欄位中找到。事實上,您可把它設定成1至4、rptKeyxxxx符號常數、或其對應的索引鍵。倘若您省略了匯出格式或檔案時,不論ShowDialog是否為偽,匯出對話方塊總是會顯示。
' Export all pages to an HTML file in the application's directory. Cookie = DataReport1.ExportReport rptKeyHTML, App.Path & "\Orders", True
若您指定的匯出檔案已存在且Overwrite為False時,匯出對話方塊亦會顯示。您可以省略附檔名,因為匯出過濾器會自動把它加上去。
注意
DataReport設計師的匯出功能可能還需要加以改善。在許多場合,執行上述的程式導致整合開發環境的當掉。這問題會隨意的出現,而筆者無法找出它的可用性。
註標 | 符號常數 | 字串 | 副檔名 | 說明 |
---|---|---|---|---|
1 | RptKeyHTML | "key_def_HTML" | *.htm, *.html | HTML |
2 | rptKeyUnicode- HTML_UTF8 | "key_def_Unicode- HTML_UTF8" | *.htm, *.html | Unicode HTML |
3 | RptKeyText | "key_def_Text" | *.txt | Text |
4 | rptKeyUnicode- Text | "key_def_Unicode- Text" | *.txt | Unicode text |
表15-2 指出四種匯出格式的索引、符號常數與索引鍵 |
建立自訂匯出格式
匯出機制是很強大的。事實上,可以藉由增加一個ExportFormat物件至ExportFormats集合中來定義一種匯出格式。此集合的Add方法需要五個參數,其分別對應到要被建立的ExportFormat物件中的屬性:
ExportFormats.Add Key, FormatType, FileFormatString, FileFilter, Template
Key是索引鍵,用來於集合中指定此份新的匯出格式。FormatType值為下列之一:0-rptFmtHTML,2-rptFmtUnicodeText或3-rptFmtUnicodeHTML_UTF8。FileFormatString為顯示於檔案過濾下拉式清單中(在匯出對話方塊內)的敘述,FileFilter是此類報表的檔案過濾條件,Template為決定報表如何排列的字串:
Private Sub DataReport_Initialize() ' Create a custom export format. Dim template As String template = "My Custom Text Report" & vbCrLf & vbCrLf _ & rptTagTitle & vbCrLf & vbCrLf _ & rptTagBody ExportFormats.Add "Custom Text", rptFmtText, _ "Custom text format (*.txt)", "*.txt", template End Sub
當設定Template屬性時,可有兩種特殊字串,當列印時會被真正的列印資料所取代。DataReport函式庫包括了這些如符號常數般的字串:rptTagTitle常數用來表示報表的標題(很像把Caption屬性設定成 %I的RptLabel控制項),rptTagBody則會被報表主體所取代。當建立HTML格式的Template字串時,可以執行任一文字屬性,就像下列的程式:
Private Sub DataReport_Initialize() ' Create a custom HTML format for exporting this report. Dim template As String Title = "Orders in May 1999" template = "<HTML>" & vbCrLf & _ "<HEAD>" & vbCrLf & _ "<TITLE>" & rptTagTitle & "</TITLE>" & vbCrLf & _ "<BODY>" & vbCrLf _ & rptTagBody & vbCrLf & _ "</BODY>" & vbCrLf & _ "</HTML>" ExportFormats.Add "Custom HTML", rptFmtHTML, _ "Custom HTML format (*.htm)", "*.htm;*.html", template End Sub
一但您增加一種自訂的ExportFormat物件時,其會出現在匯出對話方塊的下拉式清單中,然後您可以用程式碼來選擇它,就像選擇其他內建匯出格式一樣:
' Export the first page to an HTML report in custom format. Cookie = DataReport1.ExportReport "Custom Text", App.Path & "\Orders", _ True, False, rptRangeFromTo, 1, 1
執行時改變報表的格式
時常會需要建立數個類似的報表,例如一個要顯示所有在Employees資料表內資料的報表,另一個則要隱藏機密部份等。由於DataReport是可程式化的物件,絕大部份下,可用幾行程式來涵蓋這些小差異。事實上,您可以選擇報表上的控制項,然後移動他們的位置,改變其大小或可視性,或改變其屬性值,諸如Caption、ForeColor等。
在了解如何實作前,必須先知道如何參照一個Section物件、使用Sections集合與參照Section內的控制項:
' Hide the footer section corresponding to the Orders Command. DataReport1.Sections("Orders_Footer").Visible = False ' Change the background color of the lblTitle control. DataReport1.Sections("Section1").Controls("lblTitle").Caption = "May 99"
可以藉由Section數值化的註標或其名稱來參照某特定的Section。當DataReport被建立時,預設出現的Section有著通稱:Section1為報表頁首,Section2為報表頁尾,Section3為頁面頁首,Section4為頁面頁尾。涵蓋資料欄位的Section以擷取資料的Command物件名稱為名。不論如何,都可以透過屬性視窗改變Section的Name屬性。
由於DataReport控制項集合不支援Add方法(不像表單控制項集合),因此無法於執行時增加控制項。由於這樣的限制,必須在設計報表時,就得先放置所有可能需要的欄位,然後視報表的版本,將不需要的欄位隱藏起來。可藉由Section的Visible屬性隱藏整個Section,還可藉由Height屬性收縮Section高度。不過有點詭異的是:若有一些控制項是不可視的話,就無法降低Section的高度。(甚至控制項的Visible屬性為真亦同。)因此,在把控制項設成看不見後,若要使其所在的Section高度可降低的話,必須要減少控制項的Top屬性。
書上所附光碟內的程式把這些技術統合在一份報表的兩個版本上,一個會顯示每筆訂單的詳細資料,另一則否。為了讓報表成為可重複使用的,我增加了一個公用布林屬性叫ShowDetails,其可在DataReport模組外,在執行Show、PrintReport或ExportReport方法前就加以指定。底下為實作此項技術的程式碼(在DataReport模組內):
' A private member variable. Dim m_ShowDetails As Boolean Public Property Get ShowDetails() As Boolean ShowDetails = m_ShowDetails End Property Public Property Let ShowDetails(ByVal newValue As Boolean) Dim newTop As Single m_ShowDetails = newValue ' This property affects the visibility of the innermost section. Sections("Order_Details_Detail").Visible = m_ShowDetails ' It also affects the visibility of a few fields in the Orders section. ' This is the actual Top value if controls are visible; 0 otherwise. newTop = IIf(m_ShowDetails, 1870, 0) With Sections("Orders_Header") .Controls("lblProduct").Visible = m_ShowDetails .Controls("lblProduct").Top = newTop .Controls("lblUnitPrice").Visible = m_ShowDetails .Controls("lblUnitPrice").Top = newTop .Controls("lblQty").Visible = m_ShowDetails .Controls("lblQty").Top = newTop .Controls("lblDiscount").Visible = m_ShowDetails .Controls("lblDiscount").Top = newTop .Controls("lblTotal").Visible = m_ShowDetails .Controls("lblTotal").Top = newTop .Controls("shaDetailHeader").Visible = m_ShowDetails .Controls("shaDetailHeader").Top = newTop ' Setting the section's Height to 0 shrinks it as much as possible. .Height = IIf(m_ShowDetails, 2200, 0) End With End Property
圖15-11 範例程式的兩個版本,分別為每筆訂單是否包含詳細資料 |
增加動態格式與對照欄位
乍看之下,DataReport設計師所提供的功能比起Crystal Report少許多。然而事實是,當您整合DataReport與ADO的連結機制整合起來時,它就顯得相當具有潛在能力。
當您了解可用StdDataFormat物件的Format事件來控制連結欄位的格式時,就能清楚地明白這種力量。由於每次當資料自資料來源讀入時皆會引發這個事件,所以每當資料要顯示於報表時,可藉此來執行自定的程式碼。下列範例說明如何使用此技術來避開discount值為0時:
' This is used to trap the instant when a new record is read. Dim WithEvents DiscountFormat As StdDataFormat Private Sub DataReport_Initialize() ' Create a StdDataFormat object, and assign it to the txtDiscount field. Set DiscountFormat = New StdDataFormat Set Sections("Order_Details_Detail").Controls("txtDiscount"). _ DataFormat = DiscountFormat End Sub Private Sub DiscountFormat_Format(ByVal DataValue As _ StdFormat.StdDataValue) ' If the discount is zero, use a Null value instead. If CDbl(DataValue.Value) = 0 Then DataValue.Value = Null End Sub
不幸的是,在Format事件程序內,不能直接修改控制項的屬性,例如Visible、ForeColor或BackColor。當報表執行時,也不能動態地指定圖片給RptImage控制項,否則就可以顯示儲存在資料庫的圖檔。如果這些限制被去除的話,那末DataReport設計師就可能成為一個不錯的工具,對大部份的報表應都可解決。
另一個小問題是當此事件發生時,DataValue.TargetObject屬性的值為Nothing,所以無法將同一個StdDataFormat指定給其他控制項的DataFormat屬性,因為無法得知哪個欄位正在處理中。
範例程式也說明如何藉由這機制的變種來實作查閱欄位。在Initialize事件中,DataReport開啟一個Recordset指向查閱表,而在Format事件中,則將Orders資料表中的CustomerID值轉換成Customers資料表的CompanyName值:
Dim WithEvents CustFormat As StdDataFormat ' Used to look up the CustomerID field in the Customers table Dim rsCust As New ADODB.Recordset Private Sub DataReport_Initialize() ' Create a new format object, and assign it to the txtCustomer field. Set CustFormat = New StdDataFormat Set Sections("Orders_Header").Controls("txtCustomerName").DataFormat _ = CustFormat ' Open a Recordset on the Customers table. rsCust.Open "Customers", DataEnvironment1.Connection1, adOpenStatic, _ adLockReadOnly, adCmdTable End Sub Private Sub DataReport_Terminate() ' Close the Recordset. rsCust.Close Set rsCust = Nothing End Sub Private Sub CustFormat_Format(ByVal DataValue As StdFormat.StdDataValue) ' Transform a CustomerID value into the customer's CompanyName. rsCust.MoveFirst rsCust.Find "CustomerID='" & DataValue.Value & "'" If rsCust.EOF Then DataValue.Value = Null ' Match not found. Else DataValue.Value = rsCust("CompanyName") ' Match found. End If End Sub
本書關於資料庫程式部份就至本章為止。現在,您應該更了解ADO了,不論是他那神奇的力量或缺點。本書的下個部份,將告訴您如何善用類別與物件來建立ActiveX元件與控制項。若您是個資料庫的程式開發者,第十八章有關於ADO的額外資訊,包含如何建立自有的資料來源類別與OLE DB提供者等。