微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Cocoa NSUndoManager (REDO/UNDO)

@H_502_4@Cocoa NSUndoManagersg_trans.gif (REDO/UNDO)

@H_502_4@原文: http://blog.sina.com.cn/s/blog_5df7dcaf0100bp8w.html

@H_502_4@ 

@H_502_4@NSUndoManager

@H_502_4@ 

@H_502_4@使用NSUndoManaer,们可以给程序以一种优雅的风格添加undo功能. undo管理器跟踪管理一个对象的添加,编辑和删除.这些消息将会发送undo管理器去做undo. 而当我们请求做undo操作,undo管理器也会跟踪这些消息,这些消息会被记录用来做redo. 该机制使用两个NSInvocation对像堆栈来实现.

@H_502_4@ 

@H_502_4@这么早就讨论这个主题是相当沉重的.(时候一说起undo.我的头就有点大.),过因undodocument,所以我们先来学习undo是怎么工作的.这样在下一章能更好理解document构的工作流程.

@H_502_4@ 

@H_502_4@NSInvocation
正如你所想,应该有个对象能方便的封装一个消息[就是一个操作] - 包含selector,接受对象,以及所有的参数 . NSInvocation对象就是这样的对象.

@H_502_4@invocation一个非常方便的用途就是发消息. 一个对象接受到一个它没法响应的消息[没有现该方法].message-sending统不会马上抛出一个异常,它会先检查该对象是否实现了这个方法
- (void)forwardInvocation:(NSInvocation *)x
如果对象实现了该方法.那么这个消息就会被封包成对象NSInvocation-x.调用forwardInvocation:方法

@H_502_4@NSUndoManager是怎样工作的
假定一下用户打一个新的RaiseMan document,并且做了3编辑动作
.添加一条记录
.记录的名字"New Employee" 修改"Rex Fido"
.raise改成20
现每一次修改,controller将把一个要做undoinvocation添加undo栈中.简单的说:"修改的反向动作添加undo栈中". 9.1是在作了上面3修改后的undo
5df7dcafx631d86aafade.jpg 如果这时候用户Undo,那么第一个invocation将会抛出并调用.person raise设置成0.如果用户再次点Undo,那么personname将会修改"New Employee"

@H_502_4@每一次从Undo栈弹出执行一项时,反向操作将会压入到redo栈中.所以,执行了上面说的两个undo动作后,undoredo栈将会是这样的如图9.2
5df7dcafx631d875bc307.jpeg
undo manager是非常智能的,当用户做编辑动作时,undo invocation将加入到undo栈中,当用undo编辑时,undo invocation将加入到redo . 而当用redo编辑时,undo invocation又加入到undo栈中. 这样操作都是自动完成的. 们的任务仅仅是提供给undo manager要做反向操作的invocation.

@H_502_4@现在假设我们编写一个方法 makeItHotter,它的反向操作方法 makeItColder. 看看是如何undo
- (void)makeItHotter
{
    temperature = temperature + 10;
    [[undoManager prepareWithInvocationTarget:self] makeItColder];
    [self showTheChangesToTheTemperature];
}

@H_502_4@你可能猜到了,prepareWithInvocationTarget: 记录了target [self].并且返回undo manager 它自己. undo manager载了forwardInvocation: invocation-makeItColder: 加入到 undo栈中

@H_502_4@所以,们还有实现方法makeItColder
- (void)makeItColder
{
    temperature = temperature - 10;
    [[undoManager prepareWithInvocationTarget:self] makeItHotter];
    [self showTheChangesToTheTemperature];
}
们在undo manager注册了反向操作. 执行undo,makeItColder将被执行,而它的反向makeItHotter将会添加redo栈中

@H_502_4@每个栈中的invocation会是聚合的. 认的,单一事件[做了一个操作]发生时加入到栈中的所有invocation将会是聚合在一起 [这里要理解什么是invocation,简单来讲,它就是某个对象的某个方法. 所以当你做某个单一操作时,可能会涉及到多个对象,多个方法. 也就是多个invocation]. 所以,当用户的一个操作改变了多个对象时如果点undo ,那么所有的改变都会一次undo完成.

@H_502_4@们也可以来修改菜单 Undo Redo 标题. 比如使用Undo Insert来代替简单的Undo. 可以使用如下代
[undoManager setActionName:@"Insert"];

@H_502_4@那么,怎么得到一个undo manager?你可以直接创建. 过注意,NSDocument对象已经有一个自己的undo manager [它也是自己创建的哈]

@H_502_4@
RaiseMan添加Undo功能

@H_502_4@为了使用户可以使用undo功能: undo Add New Employess Delete.以及undo person对象的修改. 们必须给MyDocument添加

@H_502_4@当我计类时,我会考虑为什么要定义一个成员变量? 一定是下面的4个目的之一
1.
简单的属性: 比如学生的名字. 们一般会是数字或Nsstring,NSNumber,NSDate,NSData对象
2.
单一关系: 比如一个学生一定会有一个学校和他相关. 这和1较像,只是它的类型是一个杂对象.单一关系使用指针来实现: 学生对象有一个指向学校对象的指
3.
有序的多元关系: 比如,每个播放列表会有一系列歌曲和它关. 这些歌曲有特定的顺序. 这样的关系一般使用NSMutableArray
4.
无序的多元关系: 比如,每个部门会有一些雇,们可以对雇员按某个方式来排序(比如按照姓氏),过这样的顺序都不是本质上的顺序. 一般使用NSMutalbeSet.

@H_502_4@早些时候,们讨论了怎样使用key-vaule coding设置简单属性和单一关系
.setting 或是 getting fido值时,key-value coding使用accessor 方法.样的我们可以为有序的多元关系和无序的多元关系创建accessor方法.

@H_502_4@来看看,对象playlist一个NSMutabelArray变量来存放Song对象.如果你使用key-value coding来操作这个array对象,你将调用mutableArrayVauleForKey: .  得到一个代理对象,这个代理对象表示那个array.
id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];
int songCount = [arrayProxy count];
这个例子中.调用 count方法.代理对象将先看playlist对象有没有实 countOfSongs方法.如果有,那么就会调用方法并返回结果.如果没有,那么会调用保存songarraycount 方法9.3. 注意.方法countOfSongs的命名不仅仅是因为编码习惯: key-vaule coding机制使用这样的名字查找
5df7dcafx631d88256cdf.jpg
下面是几个例子
id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];

@H_502_4@int x = [arrayProxy count]; // is the same as
int x = [playlist countOfSongs]; // if countOfSongs exists

@H_502_4@id y = [arrayProxy objectAtIndex:5] // is the same as
id y = [playlist objectInSongsAtIndex:5]; // if the method exists

@H_502_4@[arrayProxy insertObject:p atIndex:4] // is the same as
[playlist insertObject:p inSongsAtIndex:4]; // if the method exists

@H_502_4@[arrayProxy removeObjectAtIndex:3] // is the same as
[playlist removeObjectFromSongsAtIndex:3] // if the method exists

@H_502_4@对于无序多元关系也是一样的如9.4

@H_502_4@5df7dcafx631d88c196f2.jpg
id setProxy = [teacher mutableSetValueForKey:@"students"];

@H_502_4@int x = [setProxy count]; // is the same as
int x = [teacher countOfStudents]; // if countOfStudents exists

@H_502_4@[setProxy addobject:newStudent]; // is the same as
[teacher addStudentsObject:newStudent]; // if the method exists

@H_502_4@[setProxy removeObject:expelledStudent]; // is the same as
[teacher removeStudentsObject:expelledStudent]; // if the method exists

@H_502_4@为我们绑定了array controllercontentArrayMydocument对象的employees. 所以array controller将会使用key-vaule coding添加删除person对象. 们可以使用这个机制来实现当添加person对象时添加unod invocationundo. MyDocument,m添加如下方法
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    NSLog(@"adding %@ to %@",p,employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self]
                          removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Insert Person"];
    }
    // Add the Person to the array
    [employees insertObject:p atIndex:index];
}

@H_502_4@- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    NSLog(@"removing %@ from %@",employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Delete Person"];
    }

@H_502_4@    [employees removeObjectAtIndex:index];
}

@H_502_4@NSArrayController添加或是删除Person对象时,这些方法自动调用:例如,Create New Delete 钮发送insert: remove: 消息的时候

@H_502_4@MyDocument.h中声明
- (void)removeObjectFromEmployeesAtIndex:(int)index;
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index;
由于使用了Person.所以我们需要告知编译器. MyDocument.h添加
#import <Cocoa/Cocoa.h>
@class Person;
,MyDocument.m导入Person.h
#import "Person.h"

@H_502_4@好了,们已经可以undo添加删除. 对于undo 编辑会有点复杂. 在搞定它前,编译运行我们的程序.试试undo功能. 注意,redo功能也是可用的

@H_502_4@Key-Vaule Observing
在第7,们讨论了key-vaule coding. 忆一下,key-vaule coding是一种通过变量名字读取和修改变量值的方法. key-vaule  observing是当这些改变发生时我们能得到通知.

@H_502_4@为了实现undo 编辑,们需要让document对象能够得到改变了Person对象expectedRaisepersonName通知. NSObject一个方法可以用来注册这样的通知
- (void)addobserver:(NSObject *)observer
         forKeyPath:(Nsstring *)keyPath
            options:(NSkeyvalueObservingOptions)options
            context:(void *)context;

@H_502_4@对象observer为要通知的对象,keyPath标识激活通知的改变. options义了通知包含的内容,例如,是否包含改变前的值,是否包含改变后的值. context一个随着通知一起发送的对象,可以包含任何信息.一般NULL.

@H_502_4@一个变发生,observer对象将收到下面的消息
- (void)observeValueForKeyPath:(Nsstring *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;
observer会知道那个对象的那个key path变了. change一个dictionary,其中的内容会依据在注册option指定的. 可能包含改变前的值和()变后的值. context针就是注册是的context,通常情况下,忽略它.

@H_502_4@Undo编辑

@H_502_4@第一步是将document对象注册观察它自己的person对象改变.MyDocument.m添加如下方法
- (void)startObservingPerson:(Person *)person
{
    [person addobserver:self
             forKeyPath:@"personName"
                options:NSkeyvalueObservingOptionOld
                context:NULL];

@H_502_4@    [person addobserver:self
             forKeyPath:@"expectedRaise"
                options:NSkeyvalueObservingOptionOld
                context:NULL];
}

@H_502_4@- (void)stopObservingPerson:(Person *)person
{
    [person removeObserver:self forKeyPath:@"personName"];
    [person removeObserver:self forKeyPath:@"expectedRaise"];
}

@H_502_4@添加删除Person对象是调用上面的方法
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self]
         removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Insert Person"];
    }

@H_502_4@    // Add the Person to the array
    [self startObservingPerson:p];
    [employees insertObject:p atIndex:index];
}

@H_502_4@- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Delete Person"];
    }
    [self stopObservingPerson:p];
    [employees removeObjectAtIndex:index];
}

@H_502_4@- (void)setEmployees:(NSMutableArray *)a
{
    if (a == employees)
        return;

@H_502_4@    for (Person *person in employees) {
        [self stopObservingPerson:person];
    }

@H_502_4@    [a retain];
    [employees release];
    employees = a;
    for (Person *person in employees) {
        [self startObservingPerson:person];
    }
}

@H_502_4@现编辑修改方法
- (void)changeKeyPath:(Nsstring *)keyPath
             ofObject:(id)obj
              tovalue:(id)newValue
{
    // setValue:forKeyPath: will cause the key-value observing method
    // to be called,which takes care of the undo stuff
    [obj setValue:newValue forKeyPath:keyPath];
}

@H_502_4@现当Person 对象编辑通知响应方法,
- (void)observeValueForKeyPath:(Nsstring *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    NSUndoManager *undo = [self undoManager];
    id oldValue = [change objectForKey:NSkeyvalueChangeOldKey];

@H_502_4@    // NSNull objects are used to represent nil in a dictionary
    if (oldValue == [NSNull null]) {
        oldValue = nil;
    }
    NSLog(@"oldValue = %@",oldValue);
    [[undo prepareWithInvocationTarget:self] changeKeyPath:keyPath
                                                  ofObject:object
                                                   tovalue:oldValue];
    [undo setActionName:@"Edit"];
}

@H_502_4@好了,现在编译运行程序,undo redo功能完全可以工作了.

@H_502_4@注意到了? 一旦我修改document,窗口标题栏上的红色关闭按钮会出现一个黑点来提示我们,这些改变没有被保存. 在下一个,们来学习把它们保存为文件

@H_502_4@添加后里面编辑
们的程序看上去运行的很好,过有些用户可能会抱怨"当我插入一条记录后,为什么我必须双击才能开始编辑?很明显的我一定会修改新增person的名字啊."

@H_502_4@这会有些复,我打算提供所需的代码片段,首先,MyDocument.h添加一个acton和两个成员变量
@interface MyDocument : NSDocument
{
    NSMutableArray *employees;
    IBOutlet NSTableView *tableView;
    IBOutlet NSArrayController *employeeController;
}
- (IBAction)createEmployee:(id)sender;
保存文件,(们记住一定要保存.h文件.这样新加的actionoutlet才能在Interface Builder中找到)Interface BuilderControl-drag Add New Employee钮到File's Owner(MyDocument对象). 设置actioncreateEmployee: 9.5
5df7dcafx631d8af27676.jpg

@H_502_4@Control-click file's Owner,设置好outlet tableView employeeController9.6
5df7dcafx631d8b991d3f.jpg
MyDocument.m添加 createEmployee:方法
- (IBAction)createEmployee:(id)sender
{
    NSWindow *w = [tableView window];

@H_502_4@    // Try to end any editing that is taking place
    BOOL editingEnded = [w makeFirstResponder:w];
    if (!editingEnded) {
        NSLog(@"Unable to end editing");
        return;
    }
    NSUndoManager *undo = [self undoManager];

@H_502_4@    // Has an edit occurred already in this event?
    if ([undo groupingLevel]) {
        // Close the last group
        [undo endUndoGrouping];
        // Open a new group
        [undo beginUndoGrouping];
    }
    // Create the object
    Person *p = [employeeController newObject];

@H_502_4@    // Add it to the content array of 'employeeController'
    [employeeController addobject:p];
    [p release];
    // Re-sort (in case the user has sorted a column)
    [employeeController rearrangeObjects];

@H_502_4@    // Get the sorted array
    NSArray *a = [employeeController arrangedobjects];

@H_502_4@    // Find the object just added
    int row = [a indexOfObjectIdenticalTo:p];
    NSLog(@"starting edit of %@ in row %d",row);

@H_502_4@    // Begin the edit in the first column
    [tableView editColumn:0
                      row:row
                withEvent:nil
                   select:YES];
}
不能期望你能理解没一行代.试着浏览这些方法,立即它们的基本原理. 编译运行程序吧.

@H_502_4@
思考: WindowsUndo Manager
可以把view编辑动作加入到undo manager.例如,NSTextView,可以把文字输入的动作加入到undo manager.使用Interface Builder来激活 9.7

那么text view是怎么知道使用哪一个undo manager?首先,它会delegate. NSTextViewdelegate可以现这个方法
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)tv;
接下来,它会问他的window. NSWindow一个方法
- (NSUndoManager *)undoManager;
windowdelegate可以一个方法来说明是否window可以提供undo manager
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;

@H_502_4@Undo/redo 项反应了当前key windowundo manager(key window也就是大家说的active window. Cocoa 发者叫它key 是因用户键盘输入事件由它接受)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐