為初學者講解用 Cocoa 程式設計 Mac OS X/檢查器呼叫
本節不會真正介紹有關 Cocoa 的任何新內容,但它將有助於鞏固我們已經學到的知識。其目的是為 Wikidraw 提供一個檢查器型別的介面,以便我們可以設定我們繪製的物件的屬性,例如顏色、描邊寬度等。
從我們學到的關於通知的知識中,我們可以看到如何實現這一點。每當選擇發生變化時,就會廣播一個通知,表明這一事實。檢查器可以監聽這些通知,並找出當前的選擇是什麼,然後簡單地更新其介面以匹配所選物件的屬性。
當用戶更改介面時,新屬性將應用於選擇中的物件。
建立檢查器的過程類似於工具調色盤的過程。使用單個全域性檢查器,因此它可以在 'MainMenu.nib' 檔案中。我們需要一個新的控制器物件來充當介面元素和文件之間的媒介。現在,需要指出的是,在實際應用程式中,您可能會為這樣的介面元素建立單獨的 .nib 檔案,只是為了保持事物的可管理性,甚至可以在其他應用程式中重用。但是,這樣做會帶來一些複雜性,這些複雜性並不能幫助我們瞭解 Cocoa 的功能,所以現在我們不會這樣做。但請記住,比我們即將要做的事情有更好的方法。
我們可以在 Interface Builder 中完成大部分檢查器設計,儘管這是一箇中等複雜度的使用者介面。
使用者能夠在主文件中選擇不同的物件;檢查器用於顯示所選物件的屬性並允許以互動方式更改它們。但是,使用者也可以選擇多個物件,或者一個物件也不選擇。在這些情況下我們應該怎麼做?在這個設計中,我們採用了一種簡單的方法——如果沒有選擇任何物件,檢查器將隱藏編輯屬性的控制元件並顯示“未選擇”。對於多選,我們做同樣的事情,只是顯示“多選”。只有當只有一個物件被選中時,我們才會顯示編輯控制元件。這不像一些應用程式那樣複雜——也許您會設計一個實際應用程式,以便可以一次編輯多個物件。但是,這種方法很簡單,它清楚地說明了如何處理不同的情況。
安排控制元件集以按照所描述的方式顯示和消失的一個好方法是使用選項卡式介面,但實際上沒有選項卡本身。當我們檢測到每個情況時,我們切換到選項卡的相應窗格,我們已經用我們想要的控制元件設定了這些窗格。這樣,我們就不必擔心單獨隱藏或顯示特定控制元件,這當然是可以的,但需要更復雜的程式碼。
要檢測每個情況,我們只需檢視選擇陣列中物件的計數。如果計數為零,我們知道沒有選擇任何內容。如果為一,則表示我們有一個物件,如果為任何其他數字,則表示我們有多個選擇。因此,確定選擇狀態的程式碼非常簡單。
在 IB 中,開啟 'MainMenu.nib' 檔案。在檔案中新增一個面板視窗,並使用 IB 的檢查器將其設定為標題為“檢查器”的實用程式視窗。
將一個 NSTabView 拖到視窗中。最初它將具有可見的選項卡,這將使其更容易處理。稍後我們將隱藏它們。我們需要三個選項卡——使用檢查器設定它。調整選項卡檢視的大小,使其舒適地位於視窗內。第一個選項卡將用於“未選擇”情況,因此選擇此選項卡並將文字項拖到視窗中。將文字更改為“未選擇”,並將顏色設定為淺灰色。將選項卡的識別符號更改為字串“none”。在我們的程式碼中,我們將使用這些識別符號選擇顯示的選項卡。
切換到選項卡 3。這將是多選情況,因此像以前一樣拖動文字項並將文字設定為“多選”。將此選項卡檢視的識別符號設定為“multi”。
切換到選項卡 2。這將是我們實際的編輯控制元件選項卡,我們需要相當多的控制元件。將此選項卡的識別符號設定為“std”。如螢幕截圖所示,我將其分為兩部分,描邊和填充。每個部分包含一個單選按鈕選擇和一個顏色井。描邊部分還包含一個用於設定描邊線寬的滑塊控制元件。找到所有這些控制元件並將它們拖入。整齊地排列它們。描邊寬度滑塊設定為具有 0.3 到 20.0 的範圍——這最終將成為線寬值。將標記數設定為 21 並在滑塊上方。還要選中“在滑動時持續傳送操作”,並確保“僅停留在刻度標記上”未選中。
最後,再次選擇選項卡檢視並將其設定為隱藏選項卡。調整選項卡檢視的位置和大小,使其填充面板視窗,並且每個選項卡都按您希望的方式顯示,並且您可以看到所有控制元件,並且它們整齊地排列。
現在我們需要為檢查器面板建立一個控制器。切換到主視窗的“類”選項卡,然後選擇 NSWindowController 類。選擇類——>子類化 NSWindowController。為新類命名為“InspectorController”。現在我們需要新增所有操作和出口。雙擊新類名以在 IB 檢查器中顯示操作和出口編輯器。新增以下操作
- fillColourButtonAction
- fillColourDidChange
- strokeColourButtonAction
- strokeColourDidChange
- strokeWidthDidChange
切換到出口編輯器。新增以下出口
- panelTabControl
- fillColourButtons
- fillColourWell
- strokeColourButtons
- strokeColourWell
- strokeWidthSlider
現在我們可以生成 InspectorController 的程式碼檔案。選擇類——>為 InspectorController 建立檔案。接受預設值,並在隨後的對話方塊中點選選擇。
接下來,例項化控制器。選擇新類,然後選擇類——>例項化 InspectorController。一個新例項將新增到主視窗中。
最後,我們需要連線所有出口和操作。切換到主視窗的“例項”選項卡。首先是出口:從表示 InspectorController 例項的框中控制拖動到面板中的各種控制元件。從名稱應該很清楚哪些控制元件應該連線到哪個出口。選項卡窗格本身可能很棘手——您可能需要暫時顯示選項卡才能將選項卡檢視突出顯示為目標。重要的是,“panelTabControl”出口連線到 NSTabView,而不是連線到它內部的 NSView。另外,將出口“window”連線到面板視窗本身。
接下來連線操作。從每個活動控制元件中控制拖動到 InspectorController 例項中。同樣,名稱應該表明哪些操作與哪些控制元件相關聯。只有活動控制元件需要操作——您新增到選項卡檢視中其他窗格中的文字是無源的,沒有操作。
最後,在主選單欄的“視窗”下新增一個選單命令,用於“顯示檢查器”命令。它的操作應該是 InspectorController 中的 showWindow: 操作。
一旦您滿意地連線了所有操作和出口,就儲存檔案並返回 Xcode。
我們要做的第一件事是在我們的文件中新增一個通知,以便當選擇發生變化時,任何感興趣的物件都知道這一點。檢查器將監聽此通知,並做出相應的響應。有四種方法會影響 MyDocument 中的選擇狀態
- (void) selectObject:(id) object; - (void) deselectObject:(id) object; - (void) selectAll; - (void) deselectAll;
我們需要對所有四個程式碼進行一個小修改,以確保檢查器“跟上”選擇更改。由於通知需要一個名稱,所以讓我們定義一個。
extern NSString* notifyObjectSelectionDidChange;
這放在類定義之外,在 MyDocument.h 中的“@end”語句下方
在 MyDocument.m 中,新增以下內容
NSString* notifyObjectSelectionDidChange = @"objectSelectionDidChange";
這部分程式碼出現在 “@implementation” 語句之前。我們只是聲明瞭一個全域性字串,用於表示這個特定的事件。透過在 .h 檔案中宣告它為 “extern”,我們允許其他程式碼知道這個字串,而無需關心它的實際位置。我們的 .m 檔案實際上賦予了這個字串真正的內容。
現在,按照如下方式修改選擇方法:
- (void) selectObject:(id) object
{
if([_objects containsObject:object] && ![_selection containsObject:object])
{
[_selection addObject:object];
[[NSNotificationCenter defaultCenter] postNotificationName:notifyObjectSelectionDidChange object:self];
}
}
- (void) deselectObject:(id) object
{
[_selection removeObject:object];
[[NSNotificationCenter defaultCenter] postNotificationName:notifyObjectSelectionDidChange object:self];
}
- (void) selectAll
{
[_selection setArray:_objects];
[[NSNotificationCenter defaultCenter] postNotificationName:notifyObjectSelectionDidChange object:self];
}
- (void) deselectAll
{
[_selection removeAllObjects];
[[NSNotificationCenter defaultCenter] postNotificationName:notifyObjectSelectionDidChange object:self];
}
我們所做的只是在每個方法中添加了一行程式碼,當選擇發生改變時,釋出一個我們宣告的名稱的通知。請注意,我們沒有嘗試通知發生了哪種型別的改變,只是通知它發生了改變。訊息的接收方可以呼叫我們來獲取更多資訊,如果需要的話。
在這個階段,可能值得編譯並執行應用程式,以檢查它是否仍然可以正常工作。到目前為止,我們還沒有新增任何可見的新功能。
檢查器本身的程式碼將新增到 InspectorController.m 檔案中。這是我們在上一節中用 IB 為我們建立的其中一個檔案。如果您選擇這個檔案,您將看到它已經為我們擴充套件了動作方法。然而,我們還需要另外兩個方法,因此,讓我們最初設定好它們,並正確地銷燬它們。以下是 awakeFromNib 方法和 dealloc 方法:
- (void) awakeFromNib
{
[(NSPanel*)[self window] setFloatingPanel:YES];
[(NSPanel*)[self window] setBecomesKeyOnlyIfNeeded:YES];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(selectionChanged:)
name:notifyObjectSelectionDidChange
object:nil];
}
- (void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
與我們的工具調色盤一樣,我們首先確保面板視窗浮動,並且只有在絕對必要的情況下才成為關鍵視窗。然後,我們訂閱來自 MyDocument 的通知,我們在上面設定了這個通知。為了讓這個檔案知道我們的通知名稱和 MyDocument 的方法,請在檔案頂部新增 “#import "MyDocument.h"”。
切換到 InspectorController.h 檔案,並將此方法新增到類定義中:
- (void) selectionChanged:(NSNotification*) note;
回到 .m 檔案,將這個方法擴充套件為如下:
- (void) selectionChanged:(NSNotification*) note
{
MyDocument* doc = (MyDocument*)[note object];
NSArray* sel = [doc selection];
NSString* tab;
NSLog(@"selection changed, objects selected = %d", [sel count]);
switch([sel count])
{
case 0:
tab = @"none";
break;
case 1:
tab = @"std";
break;
default:
tab = @"multi";
break;
}
[panelTabControl selectTabViewItemWithIdentifier:tab];
}
這就是選擇改變的通知被實際響應的地方。它在主文件中的選擇狀態發生改變時被呼叫。它首先要做的事情是找出哪個文件發出了訊息。訊息的傳送者是通知本身的一部分,可以使用 “doc = [note object]” 訊息獲得。接下來,我們需要選擇,所以我們向文件請求它, “sel = [doc selection]”。然後,我們可以簡單地計算物件的數量來確定我們需要顯示檢查器的哪個面板。根據選擇的 0 個、1 個或多個物件,我們使用所需的識別符號設定字串 “tab”(您會回憶起我們在 Interface Builder 中設定了它——如果它不起作用,請檢查識別符號是否匹配)。最後,我們只需使用識別符號設定 tab 即可。我們有一個指向 tab 檢視的引用,因為它是我們其中一個出口,它是由系統自動設定的。
現在我們可以測試它了。編譯並執行應用程式。使用選單命令顯示工具調色盤和檢查器調色盤。在主文件中建立一些物件。使用選擇工具選擇不同的物件,選擇多個物件和沒有物件——驗證檢查器是否根據選擇狀態顯示了相應的控制元件。
下一步是將編輯控制元件連線到選定的物件,以便我們可以編輯物件的屬性。首先,我們編寫一些程式碼來設定控制元件的狀態,使其與選定物件的當前狀態匹配。然後,我們填寫動作方法,以便可以更改物件的屬性。
在 InspectorController.h 檔案中,新增以下方法定義:
- (void) setupWithShape:(WKDShape*) shape;
現在,因為我們宣告它接受一個 WKDShape* 引數,所以我們需要這個檔案“知道”這個類。我們可以在這裡使用 #import WKDShape.h,但這意味著任何只需要知道檢查器的檔案也會引入形狀檔案,而它可能不需要。因此,為了提高效率並減少相互依賴關係,我們在此階段“前向宣告”這個類。這很簡單:
@class WKDShape;
在 “@interface” 語句之前新增該行程式碼。它只是說在某個地方有一個叫做 WKDShape 的類。在這一點上,它不需要 WKDShape 的任何內部細節,因此它避免了匯入整個檔案的效率低下。
回到 .m 檔案,我們將這個方法擴充套件為如下:
- (void) setupWithShape:(WKDShape*) shape
{
NSColor* fill = [shape fillColour];
NSColor* strk = [shape strokeColour];
if ( fill )
{
[fillColourButtons selectCellWithTag:0];
[fillColourWell setEnabled:YES];
[fillColourWell setColor:fill];
}
else
{
[fillColourButtons selectCellWithTag:1];
[fillColourWell setEnabled:NO];
}
if ( strk )
{
[strokeColourButtons selectCellWithTag:0];
[strokeColourWell setEnabled:YES];
[strokeColourWell setColor:strk];
}
else
{
[strokeColourButtons selectCellWithTag:1];
[strokeColourWell setEnabled:NO];
}
[strokeWidthSlider setFloatValue:[shape strokeWidth]];
}
這真的非常明顯——它只是獲取傳遞進來的形狀物件的填充和描邊屬性,並使用它們透過控制器中的出口設定控制元件的狀態。如果沒有與描邊或填充關聯的顏色,則使用單選按鈕的標籤來選擇 “無” 選項。在這種情況下,顏色井也被停用。
由於這段程式碼確實需要 WKDShape 的內部細節,因此 .m 檔案需要 #import WKDShape.h,因此將該行程式碼新增到檔案頂部。
現在,透過修改 “selectionChanged:” 方法來連線這個方法:
case 1: tab = @"std"; [self setupWithShape:[sel objectAtIndex:0]]; break;
因為在這種情況下我們知道選擇只包含一個物件,所以它一定是該陣列中索引為 0 的物件。我們可以設定控制元件,然後再切換到 tab 面板,而不會出現任何問題。
編譯並執行應用程式,驗證現在當選擇一個物件時,檢查器是否顯示了預設顏色,即黑色描邊和白色填充。
最後,讓我們讓檢查器能夠互動式地編輯選定物件。
編輯形狀
[edit | edit source]我們首先需要做幾件事。我們有一堆不同的動作方法,它們都連線到不同的檢查器控制元件,但它們都影響同一個選定的物件。因此,我們需要跟蹤這個物件,以便我們隨時可以編輯它,只要它保持被選中。為此,我們將向它新增一個引用,作為檢查器類的成員變數。所以在 InspectorController.h 中,新增:
WKDShape* editShape;
作為成員變數。為了安全起見,我們還將新增一個叫做 setShape: 的方法,它處理形狀的選擇改變時通常的保留和釋放。我們可以設定它,使其不需要保留,但這樣的話,我們必須小心,永遠不能存在任何可能導致出現過時引用的情況。這種方式更簡單,更安全,也是“Cocoa 方式”。所以,將 setShape: 擴充套件為如下:
- (void) setShape:(WKDShape*) shape
{
[shape retain];
[editShape release];
editShape = shape;
}
在 setupWithShape: 方法的頂部新增對 [self setShape:shape] 的呼叫。現在,我們可以隨時安全地引用 editShape。
我們還需要做最後一件事來準備。您會回憶起,顯示形狀的檢視負責繪製它們,但我們將直接編輯形狀的屬性。為了讓更改立即可見,需要有一種方法可以告訴檢視在任何東西發生變化時重新整理形狀的邊界矩形。我們不想讓檢查器承擔這個任務,因為它會建立檢查器和我們的檢視類之間不必要的依賴關係。相反,形狀本身需要能夠標記正在繪製它們的檢視的必要重新整理。為了實現這一點,我們需要另一個通知。當形狀的狀態發生改變時,它會發佈一個通知。檢視透過獲得形狀的邊界並重新整理螢幕上的那個區域來響應這個通知。因為檢視最初建立了形狀,所以建立通知非常簡單,檢查器不扮演任何角色。
在 WKDShape.m 中,新增方法:
- (void) repaint
{
[[NSNotificationCenter defaultCenter] postNotificationName:notifyShapeRequiresRefresh object:self];
}
在 .h 檔案中,宣告這個方法,以及一個用於 “notifyShapeRequiresRefresh” 的 extern NSString*。在 .m 檔案中,讓這個字串取一些合適的值——我使用了字串 “repaintMe!”——但它可以是任何唯一的值。
在每個更改屬性的方法中,我們需要新增對 [self repaint]; 的呼叫,以便釋出通知。
現在,在 WKDDrawView 中,我們需要為這個通知新增一個響應者。新增以下方法:
- (void) repaintNotification:(NSNotification*) note
{
WKDShape* shape = (WKDShape*)[note object];
[self setNeedsDisplayInRect:[shape drawBounds]];
}
最後,在 WKDDrawView 的 initWithFrame: 方法中新增一行程式碼,以便它訂閱這些通知:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(repaintNotification:) name:notifyShapeRequiresRefresh object:nil];
這允許檢視在任何形狀物件的屬性發生變化時響應來自該形狀物件的重新整理請求。在這個方案中,如寫的那樣,有一個小缺陷;細心的讀者可能已經注意到了。它與多個文件有關。目前,一個檢視無法判斷形狀是否真正屬於它。然而,這種影響是無害的——這僅僅意味著比實際需要的更多重新整理,如果有多個文件的話。您可能想考慮如何解決這個問題。
回到檢查器。現在形狀可以在需要時重新整理自己,我們也有一個方法來跟蹤我們正在編輯的物件,只需將每個控制元件的動作方法連線起來,使它們做正確的事情。
- (IBAction) fillColourButtonAction:(id) sender
{
int tag = [[sender selectedCell] tag];
if ( tag == 1 )
{
[editShape setFillColour:nil];
[fillColourWell setEnabled:NO];
}
else
{
[fillColourWell setEnabled:YES];
[editShape setFillColour:[fillColourWell color]];
}
}
- (IBAction) fillColourDidChange:(id) sender
{
[editShape setFillColour:[sender color]];
}
- (IBAction) strokeColourButtonAction:(id) sender
{
int tag = [[sender selectedCell] tag];
if ( tag == 1 )
{
[editShape setStrokeColour:nil];
[strokeColourWell setEnabled:NO];
}
else
{
[strokeColourWell setEnabled:YES];
[editShape setStrokeColour:[strokeColourWell color]];
}
}
- (IBAction) strokeColourDidChange:(id) sender
{
[editShape setStrokeColour:[sender color]];
}
- (IBAction) strokeWidthDidChange:(id) sender
{
[editShape setStrokeWidth:[sender floatValue]];
}
這應該很清楚——我們提取傳送者的值,並將其傳遞給相關的屬性。對於單選按鈕,如果選擇 “無” 按鈕(tag == 1),則傳遞 nil,否則傳遞當前顏色井的顏色。我們還在這裡實現了一些 UI 狀態,如果選擇 “無” 選項,則停用顏色井。
編譯並執行進行測試——您應該發現檢查器現在是完全互動式的。選定的物件使用 repaint 通知重新整理自己,因此檢查器可以自由地簡單地設定相關的屬性,而不必關心它如何處理顯示。這是 MVC 的正確功能分離——控制器(檢查器)更改資料模型(形狀),資料模型向檢視傳送更改標誌,檢視從資料模型獲取它需要的任何內容來更新螢幕。這就是 MVC 的工作原理。