自定义NSCell内的NSButtonCell

10

在我的Cocoa应用程序中,我需要为NSTableView创建一个自定义的NSCell。这个NSCell子类包含一个自定义的NSButtonCell来处理点击事件(以及两个或三个NSTextFieldCells用于文本内容)。以下是我代码的简化示例。

@implementation TheCustomCell

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
   // various NSTextFieldCells
   NSTextFieldCell *titleCell = [[NSTextFieldCell alloc] init];
   ....
   // my custom NSButtonCell
   MyButtonCell *warningCell = [[MyButtonCell alloc] init];
   [warningCell setTarget:self];
   [warningCell setAction:@selector(testButton:)];
   [warningCell drawWithFrame:buttonRect inView:controlView];
}

我卡在这个问题上了:获取这个NSCell内部的Button(更准确地说是NSButtonCell)的最佳/正确方法是什么? "work"的意思是:当单击时触发指定的操作消息并显示备用图像。默认情况下,单击按钮不会产生任何效果。

关于这个主题的信息/读物很难找到。我在网上找到的唯一帖子指向实现

- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp; 

这样做是否正确?实现 trackMouse: 在我的包含 NSCell 中?然后将事件转发给 NSButtonCell?当它被点击时,我本来期望 NSButtonCell 自己知道该怎么做(我看到 trackMouse: 方法更多地是与跟踪鼠标移动有关,而不是作为“标准”点击行为的训练轮)。但似乎当它包含在一个单元格中时并没有这样做...... 似乎我还没有完全理解自定义单元格的整体情况;-)

如果有人能够根据自己的经验回答这个问题(或者指向一些教程之类的东西),并告诉我是否正确,我会很高兴的。

提前感谢, Tobi

2个回答

8

最低要求是:

  • 左键按下按钮后,只要鼠标在其上方,按钮就必须显示为已按下。
  • 如果鼠标随后释放在按钮上,则您的单元格必须发送相应的操作消息。

要使按钮看起来被按下,您需要根据需要更新按钮单元格的highlighted属性。仅更改状态是不够的,但您想要的是,仅当其状态为NSOnState时,按钮才会突出显示。

要发送操作消息,您需要知道何时释放鼠标,然后使用-[NSApplication sendAction:to:from:]发送消息。

为了能够发送这些消息,您需要连接到NSCell提供的事件跟踪方法中。请注意,所有这些跟踪方法(除了最终的-stopTracking:...方法)都返回一个布尔值来回答问题:“您是否要继续接收跟踪消息?”

最后的关键是,为了收到任何跟踪消息,您需要实现-hitTestForEvent:inRect:ofView:并返回适当的位掩码NSCellHit...值。具体而言,如果返回的值没有NSCellHitTrackableArea值,则不会收到任何跟踪消息!

因此,在高层次上,您的实现将看起来像:

- (NSUInteger)hitTestForEvent:(NSEvent *)event
                       inRect:(NSRect)cellFrame
                       ofView:(NSView *)controlView {
    NSUInteger hitType = [super hitTestForEvent:event inRect:cellFrame ofView:controlView];

    NSPoint location = [event locationInWindow];
    location = [controlView convertPointFromBase:location];
    // get the button cell's |buttonRect|, then
    if (NSMouseInRect(location, buttonRect, [controlView isFlipped])) {
        // We are only sent tracking messages for trackable areas.
        hitType |= NSCellHitTrackableArea;
    }
    return hitType;
}

+ (BOOL)prefersTrackingUntilMouseUp {
   // you want a single, long tracking "session" from mouse down till up
   return YES;
}

- (BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView {
   // use NSMouseInRect and [controlView isFlipped] to test whether |startPoint| is on the button
   // if so, highlight the button
   return YES;  // keep tracking
}

- (BOOL)continueTracking:(NSPoint)lastPoint at:(NSPoint)currentPoint inView:(NSView *)controlView {
   // if |currentPoint| is in the button, highlight it
   // otherwise, unhighlight it
   return YES;  // keep on tracking
}

- (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag {
   // if |flag| and mouse in button's rect, then
   [[NSApplication sharedApplication] sendAction:self.action to:self.target from:controlView];
   // and, finally,
   [buttonCell setHighlighted:NO];
}

1
你在哪里告诉表格告诉它的数据源按钮已被选中? - Richard
1
@Jeremy W. Sherman:这可能是一个愚蠢的问题,但是你如何获取“按钮电池的|buttonRect|”?我尝试了各种方法,比如[button frame],但似乎不起作用... - houbysoft

5
NSCell子类的目的是将渲染和处理常见UI元素(控件)的责任与NSView类的视觉和事件层次结构责任分开。这种配对允许每个组件提供更大的专业化和可变性,而不会给另一个带来负担。看看在Cocoa中可以创建的大量NSButton实例。如果没有这种功能上的分裂,想象一下会存在多少个NSButton子类!
使用设计模式语言来描述角色:一个NSControl充当外观,将其组成细节隐藏在其客户端中,并将事件和呈现消息传递给其作为代理的NSCell实例。
因为您的NSCell子类包括其组成部分中的其他NSCell子类实例,它们不再直接从视图层次结构中的NSControl实例接收这些事件消息。因此,为了使这些单元实例从事件响应链(视图层次结构的)接收事件消息,您的单元实例需要传递这些相关事件。您正在重新创建NSView层次结构的工作。
这并不一定是件坏事。通过以NSCell形式复制NSControl(及其NSView超类)的行为,您可以通过位置、事件类型或其他标准过滤传递给子单元的事件。缺点是在构建过滤和管理机制时复制NSView/NSControl的工作。
因此,在设计界面时,您需要考虑NSButtonCell(以及NSTextFieldCell)在正常视图层次结构中是否更适合作为NSControl或作为NSCell子类中的子单元。最好利用已经存在于代码库中的功能,而不是不必要地重新发明它(并继续后续维护)。

@Huperniketes 如果我要为表格实现自定义单元格(在 Lion 之前使用新的表格 NSView 单元格功能),那么我基本上被迫模仿我嵌入的每个控件的行为,而只是将单元格用于绘图部分。这正确吗? - David
1
@David,我不太清楚你的问题。如果你是在问如何将NSView子类嵌入到表视图中的自定义单元格中,那么你的单元格需要符合NSControl协议,可以处理来自视图子类本身的消息,将它们传递给表视图,或者将它们留作存根。你不应该需要将NSControl子类嵌入到自定义单元格中,因为通常可以直接使用控件的单元格本身。至于单元格的行为,你可以使用单元格来绘制部分内容、查找被点击的部分等。 - Huperniketes
@Huperniketes,我想学习的是如何创建一个包含其他“控件”的单元格。看起来我可以使用NSButtonCell来绘制“控件”,但实际上按钮功能必须重新创建,即我必须检测点击(对于按钮)和鼠标拖动(对于NSSlider)等操作。为了实现这个我觉得需要花费很多时间,而且我认为这个应该更简单/可重复使用。我有什么遗漏的地方吗? - David
1
@David,这里涉及到两类对象:控件和单元格。单元格(例如NSButtonCell、NSImageCell、NSTokenFieldCell等)完成了大部分工作:绘制、命中测试、计算单元格大小等等。但是,它们缺乏与视图层次结构的连接:屏幕位置、从视图(一个NSResponder子类)层次结构获取事件等等。这由NSControl提供,它作为代理将消息重定向到单元格,并在有多个单元格或控件时作为调度程序。你的单元格可以包含单元格或控件,但必须将正确的消息传递到相应的对象。 - Huperniketes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接