面向初學者的 Cocoa Mac OS X 程式設計 / 歸檔
我們已經討論了一些關於歸檔的一般原則,現在我們將為我們的繪圖文件實現它。一旦我們有了歸檔形狀的能力,就會發現實現諸如剪下和貼上之類的操作變得直截了當。
您會記得,當我們在 MyDocument 中新增程式碼以將文件讀寫到檔案時,我們使用了一對名為 NSKeyedArchiver 和 NSKeyedUnarchiver 的物件。在那時,我們只是將整個 'objects' 陣列作為所謂的 _根_ 物件進行歸檔。我們並不關心幕後發生了什麼 - 歸檔器將陣列轉換為一個 NSData 物件,我們將其傳遞給 Cocoa,Cocoa 將資料寫入檔案。當我們開啟檔案時,解歸檔器重建了我們的陣列,然後我們將其用作我們的 objects 陣列。所以我們應該能夠將繪圖儲存為檔案並再次開啟它,對嗎?不,還沒。問題是到目前為止我們還沒有歸檔形狀的實際屬性。WKDShape 是我們設計的類,因此 Cocoa 還不知道如何對其進行歸檔。因此,如果我們嘗試儲存我們的繪圖,我們會很失望地發現再次開啟它們時會得到空白文件。
我們透過編寫一些程式碼來告訴 Cocoa 如何歸檔我們的形狀。我們需要以下兩種方法
- (void) encodeWithCoder:(NSCoder*) coder; - (id) initWithCoder:(NSCoder*) coder;
這兩個方法是 _正式_ 協議 NSCoding 的一部分。我們已經遇到過非正式協議,它只是我們之間達成一致要實現的一些方法。正式協議類似,除了它被嚴格執行 - 實現正式協議的物件_必須_這樣做。為了告訴 Cocoa 我們想要成為正式協議的一部分,我們修改我們的類宣告如下
@interface WKDShape : NSObject <NSCoding>
這表明我們將要實現正式協議 _NSCoding_。如果我們沒有這樣做,我們將在編譯時得到一個錯誤,並且將無法構建我們的應用程式。
之前我們使用了 NSKeyedArchiver 及其對應的 NSKeyedUnarchiver。檢查這兩個類的標頭檔案將顯示它們實際上是更通用類 NSCoder 的子類。這就是傳遞給上面兩種方法的內容,這兩個方法是實現 NSCoding 協議所必需的。我們只需要告訴編碼器要新增哪些屬性到其歸檔中,它會完成其餘工作。我們給每個屬性一個名稱或 _鍵_,然後使用該鍵稍後檢索相同的屬性。我們需要為每個形狀儲存的屬性是
- 它的尺寸
- 它的位置
- 它的填充和描邊顏色
- 它的描邊寬度
NSCoder 有 encodeObject:ForKey: 和 encodeFloat:forKey: 之類的方法可以處理繁重的工作。所以這裡是我們歸檔方法
- (void) encodeWithCoder:(NSCoder*) coder
{
[coder encodeRect:[self bounds] forKey:@"bounds"];
[coder encodeObject:[self fillColour] forKey:@"fill_colour"];
[coder encodeObject:[self strokeColour] forKey:@"stroke_colour"];
[coder encodeFloat:[self strokeWidth] forKey:@"stroke_width"];
}
- (id) initWithCoder:(NSCoder*) coder
{
[self init];
[self setBounds:[coder decodeRectForKey:@"bounds"]];
[self setFillColour:[coder decodeObjectForKey:@"fill_colour"]];
[self setStrokeColour:[coder decodeObjectForKey:@"stroke_colour"]];
[self setStrokeWidth:[coder decodeFloatForKey:@"stroke_width"]];
return self;
}
第一個,encodeWithCoder,在儲存時使用。屬性使用這裡給出的鍵儲存到編碼器中,這些鍵只是標識屬性的字串。解碼方法相反。請注意,它是一個 init 方法,這是有道理的 - 物件由編碼器從資料流中建立,因此它需要使用儲存的屬性進行初始化。通常,物件將在做任何其他事情之前呼叫 [super initWithCoder:coder] 和 [super encodeWithCoder:coder],以便所有子類的屬性也按需儲存。在本例中,因為我們從 NSObject 繼承而來,而 NSObject 沒有實現 <NSCoding>,所以我們不需要這樣做(實際上這樣做會出錯)。這就是為什麼在本例中我們呼叫 [self init] 而不是 [super init] 的原因。
現在,如果您編譯並執行,您會發現將繪圖儲存到檔案是可行的 - 當您再次開啟該檔案時,您的繪圖會完全按儲存時的樣子重新出現,包括所有屬性,例如顏色等。
剪下和貼上利用歸檔,以便我們能夠在文件之間移動物件等。現在我們能夠歸檔 WKDShape,我們也能夠可靠且輕鬆地歸檔任何這些形狀集。回想一下,NSKeyedArchiver 將某種“根物件”轉換為 NSData 物件,這只是一塊包含該物件編碼版本的二進位制資料。NSData 在儲存文件的情況下寫入檔案,但如果我們將其寫入名為 _剪貼簿_ 的物件,我們就將該資料放到剪貼簿,從而實現剪下或複製。貼上是逆向操作,類似於解歸檔檔案;我們解歸檔剪貼簿資料並將由此建立的物件新增到文件中。
剪下和貼上是命令,因此我們在 MyDocument 中將其實現為操作方法。將以下內容新增到 MyDocument.h 中的類定義中並儲存該檔案
- (IBAction) cut:(id) sender; - (IBAction) copy:(id) sender; - (IBAction) paste:(id) sender; - (IBAction) delete:(id) sender;
我們將實現所有四個方法。但是請注意,剪下等效於複製後刪除,而刪除只是刪除選擇中的物件,因此唯一“困難”的工作是在 copy: 和 paste: 方法中。在我們實現它們之前,讓我們在 IB 中將它們連線起來。
雙擊 'MainMenu.nib' 以開啟它(如果它尚未在 IB 中開啟),然後雙擊 'FirstResponder' 圖示。回想一下,每當可以在依賴於上下文的各種地方實現命令時,我們都會使用 FirstResponder。這正是這種情況,因為剪下和貼上等可以在各種物件中以各種方式實現。碰巧的是,您會發現我們不需要做任何事情 - 當建立 Mainmenu.nib 時,剪下、貼上等都是為您預先連線的。驗證選單命令是否確實連線到這些方法。
在 Xcode 中,在 MyDocument.m 中填充這四個方法。現在,不要新增任何程式碼。編譯並執行專案。您現在會看到“編輯”選單中提供了“剪下”和“貼上”命令。實際上,我們希望這些命令僅在有東西可以選擇實際剪下時才可用,但現在我們將接受預設行為,直到我們構建並測試程式碼。
在 Cocoa 中負責剪貼簿行為的物件是 NSPasteboard。實際上,存在用於不同任務的多個剪貼簿,但我們需要的是基本剪貼簿,稱為“通用”剪貼簿。當我們收到“剪下”命令時,我們將選擇歸檔到 NSData 物件中,然後將此資料放到通用剪貼簿。因為此資料是我們應用程式私有的,所以我們不必擔心在此階段將其轉換為其他應用程式可讀的形式,例如 TIFF 或 PDF。
以下是我們編輯方法的實現
- (IBAction) cut:(id) sender
{
[self copy:sender];
[self delete:sender];
}
- (IBAction) copy:(id) sender
{
if ([[self selection] count] > 0 )
{
NSData* clipData = [NSKeyedArchiver archivedDataWithRootObject:[self selection]];
NSPasteboard* cb = [NSPasteboard generalPasteboard];
[cb declareTypes:[NSArray arrayWithObjects:@"wikidrawprivate", nil] owner:self];
[cb setData:clipData forType:@"wikidrawprivate"];
}
}
- (IBAction) paste:(id) sender
{
NSPasteboard* cb = [NSPasteboard generalPasteboard];
NSString* type = [cb availableTypeFromArray:[NSArray arrayWithObjects:@"wikidrawprivate", nil]];
if ( type )
{
NSData* clipData = [cb dataForType:type];
NSArray* objects = [NSKeyedUnarchiver unarchiveObjectWithData:clipData];
NSEnumerator* iter = [objects objectEnumerator];
id obj;
[self deselectAll];
while( obj = [iter nextObject])
{
[self addObject:obj];
[self selectObject:obj];
[(WKDShape*)obj repaint];
}
}
}
- (IBAction) delete:(id) sender
{
NSArray* sel = [[self selection] copy];
NSEnumerator* iter = [sel objectEnumerator];
id obj;
while ( obj = [iter nextObject])
[self removeObject:obj];
[sel release];
}
cut: 只是 copy: 後跟 delete
copy: 首先檢查選擇是否為空,如果不是,它使用鍵控歸檔器將選擇歸檔到 NSData 物件中。然後,它獲取通用剪貼簿。要將資料放到剪貼簿上,您首先要宣告資料的型別。這允許將多個表示形式放置在同一資料的剪貼簿中 - 接收方可以決定哪種表示形式最適合它 - 例如,圖形程式可能想要資料的 TIFF 影像,而文字處理器可能想要文字表示形式等。這裡,我們只有一個表示形式,它是一種名為“wikidrawprivate”的私有格式。因此,實際上“列表”中只有一項。一旦我們聲明瞭型別,我們就可以將資料放到與該型別關聯的剪貼簿上。
paste: 反轉此過程。首先,它獲取通用剪貼簿並檢查是否確實存在型別為“wikidrawprivate”的資料。如果沒有,則什麼也不做,因此它不會嘗試貼上它無法理解的來自另一個應用程式的資料。然後,我們從剪貼簿下載資料並解歸檔它。我們知道它是一個形狀物件的陣列 - 實際上覆制的選擇包含的內容是什麼。我們需要將這些物件新增到文件中,所以我們只需遍歷陣列並新增和選擇遇到的物件。我們還首先取消選擇文件中的所有物件,以便使用者獲得預期的結果 - 粘貼後的物件在粘貼後被選中。
delete: 方法從文件中刪除所選物件。它透過遍歷選擇陣列的副本來做到這一點。這裡需要一個副本,因為刪除一個物件也會取消選擇它 - 如果我們沒有建立副本,我們將在遍歷的同一個陣列上進行修改,並且事情不會正常工作。一旦我們將物件從文件中刪除,我們就可以完成副本,因此我們可以釋放它。
編譯並執行專案,並確認您有能力在不同的文件之間剪下和貼上物件。
進一步練習
- 修改程式碼,使其還匯出選擇的 PDF 表示形式。這有點複雜,但並不難。提示:檢視 NSView 的 writePDFInsideRect:toPasteboard: 方法
- 更復雜:新增拖放行為。這與剪下和貼上幾乎相同,並使用“拖放”剪貼簿。覆蓋 NSView 的方法以在建立拖放資料時以及在接收拖放到檢視的資料時收到通知。