跳转到内容

为初学者讲解用 Cocoa 编程 Mac OS X/检查器调用

来自 Wikibooks,开放世界中的开放书籍

上一页:更多形状 | 下一页:归档

本节不会真正介绍有关 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 的工作原理。

上一页:更多形状 | 下一页:归档

华夏公益教科书