子类化 NSSlider:需要解决丢失鼠标向上事件的问题(Cocoa OSX)

发布于 2024-09-27 08:37:48 字数 265 浏览 7 评论 0原文

我正在尝试对 NSSlider 进行子类化以创建一个称为滚轮的控件。基本上我需要的是一个滑块,它总是从中间开始,当它移动到左侧或右侧时,它会经常发送通知(由可以设置的属性确定),通知其容器其当前值,然后当您放开旋钮,它会回到中间。我希望实现将滑块返回到中间并停止在滑块的 mouseUp 事件中发送通知的功能,但似乎由于某种原因,苹果在滑块上的 mouseDown 事件之后禁用了 MouseUp 事件并处理所有滑块功能处于较低水平。无论如何我可以恢复 mouseUp 事件吗?如果没有,谁能建议一个合理的解决方法?

I am trying to subclass NSSlider to create a control called a jog dial. Basically what I need is a slider which always starts at the middle and when it is moved to the left or right it will send notifications every so often (determined by an attribute one can set) informing its container of its current value, then when you let you go of the knob, it will return to the middle. I was hoping to implement the functionality to return the slider to the middle and stop sending notifications in the mouseUp event of the slider but it seems that for some reason apple disables the MouseUp event after a mouseDown event on the slider and handles all the slider functionality at a lower level. Is there anyway I can get the mouseUp event back? If not can anyone suggest a reasonable workaround?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

小巷里的女流氓 2024-10-04 08:37:48

每当您发现超类的 mouseDragged:mouseUp: 实现没有被调用时,很可能是因为该类的 mouseDown: 实现进入一个跟踪循环。对于许多 NSControl 子类(包括 NSSlider)来说确实如此。

检测鼠标松开的更好方法是对单元进行子类化并覆盖适当的跟踪方法。在这种情况下,您可能需要 - (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag,但是 startTracking :continueTracking: 变体可能对您想要做的事情也很有用。

Whenever you notice that a superclass's implementation of mouseDragged: or mouseUp: is not getting called, it's most likely because the class's implementation of mouseDown: enters a tracking loop. This is certainly true of many NSControl subclasses including NSSlider.

A better way to detect a mouse up is to subclass the cell and override the appropriate tracking method. In this case, you probably want - (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag, but the startTracking: and continueTracking: variants might prove useful as well for what you're trying to do.

云淡风轻 2024-10-04 08:37:48

对于这种情况,我使用(但不是发明)一个技巧。首先,在 IB 中,将滑块指定为“连续”,以便在滑块移动时您会收到操作消息。然后,在action方法中,执行以下操作:

[NSObject cancelPreviousPerformRequestsWithTarget: self
  selector: @selector(finishTrack) object: nil ];
[self performSelector: @selector(finishTrack) withObject: nil
  afterDelay: 0.0];

释放鼠标后,将调用finishTrack方法。

There is a trick that I use (but didn't invent) for such situations. First, in IB, designate the slider as "continuous", so that you'll get action messages as the slider is moved. Then, in the action method, do this:

[NSObject cancelPreviousPerformRequestsWithTarget: self
  selector: @selector(finishTrack) object: nil ];
[self performSelector: @selector(finishTrack) withObject: nil
  afterDelay: 0.0];

After the mouse is released, the finishTrack method will be called.

你的心境我的脸 2024-10-04 08:37:48

这对我有用(并且比继承 NSSlider 更容易):

- (IBAction)sizeSliderValueChanged:(id)sender {
    NSEvent *event = [[NSApplication sharedApplication] currentEvent];
    BOOL startingDrag = event.type == NSLeftMouseDown;
    BOOL endingDrag = event.type == NSLeftMouseUp;
    BOOL dragging = event.type == NSLeftMouseDragged;

    NSAssert(startingDrag || endingDrag || dragging, @"unexpected event type caused slider change: %@", event);

    if (startingDrag) {
        NSLog(@"slider value started changing");
        // do whatever needs to be done when the slider starts changing
    }

    // do whatever needs to be done for "uncommitted" changes
    NSLog(@"slider value: %f", [sender doubleValue]);

    if (endingDrag) {
        NSLog(@"slider value stopped changing");
        // do whatever needs to be done when the slider stops changing
    }
}

This works for me (and is easier than subclassing NSSlider):

- (IBAction)sizeSliderValueChanged:(id)sender {
    NSEvent *event = [[NSApplication sharedApplication] currentEvent];
    BOOL startingDrag = event.type == NSLeftMouseDown;
    BOOL endingDrag = event.type == NSLeftMouseUp;
    BOOL dragging = event.type == NSLeftMouseDragged;

    NSAssert(startingDrag || endingDrag || dragging, @"unexpected event type caused slider change: %@", event);

    if (startingDrag) {
        NSLog(@"slider value started changing");
        // do whatever needs to be done when the slider starts changing
    }

    // do whatever needs to be done for "uncommitted" changes
    NSLog(@"slider value: %f", [sender doubleValue]);

    if (endingDrag) {
        NSLog(@"slider value stopped changing");
        // do whatever needs to be done when the slider stops changing
    }
}
游魂 2024-10-04 08:37:48

看来 Kperryua 的想法将提供最干净的解决方案,所以我将其标记为已接受的答案,但我最终使用了一些适合我的具体情况的黑客,所以我想我也可以分享它。

我正在制作的应用程序需要跨平台,因此我使用 Cocotron(一个在 Windows 上实现大部分 Cocoa API 的开源项目)来实现此目标,而 cocotron 不支持上面提到的 stopTracking:... 方法。

幸运的是,在玩一个小测试程序时,我们发现,如果您重写 NSSlider 的 mouseDown 方法并调用该方法的 super,它不会返回,直到释放鼠标按钮,因此您可以将调用 super 后,任何代码都应位于 mouseDown 方法内的 mouseUp 中。这是一个有点肮脏的黑客,但它似乎是唯一适用于我们的情况的解决方案,因此我们将不得不采用它。

It seems that Kperryua's idea would provide the cleanest solution so I will mark that as the accepted answer, but I ended using a bit of a hack that worked for my specific situation so I thought I might share that as well.

The app that I am making needs to be cross platform so I am using Cocotron (an open source project that implements much of the Cocoa API on Windows) to achieve this goal and cocotron does not support the stopTracking:... method mentioned above.

Luckily, while playing around with a little test program we made it was discovered that if you override the the mouseDown method of the NSSlider and call the super of that method, it does not return until the mouse button is released, so you can just put whatever code should be in mouseUp inside the mouseDown method after the call to super. This is a bit of a dirty hack but it seems to be the only solution that will work in our case so we are going to have to go with it.

筑梦 2024-10-04 08:37:48

以防万一有人想知道如何在 Swift 3 中使用 NSSlider 实现这一点,即状态设置为连续:

@IBOutlet weak var mySlider: NSSlider!

...

@IBAction func handlingNSSliderChanges(_ sender: Any) {

    // Perform updates on certain events
    if sender is NSSlider{
        let event = NSApplication.shared().currentEvent

        if event?.type == NSEventType.leftMouseUp {
            print("LeftMouseUp event inside continuous state NSSlider")
        }
    }

    // Do continuous updates
    if let value = mySlider?.integerValue{
        demoLabel.stringValue = String(value)
    }
}

...

Just in case someone is wondering how to achieve this in Swift 3 with an NSSlider thats state is set to continuous:

@IBOutlet weak var mySlider: NSSlider!

...

@IBAction func handlingNSSliderChanges(_ sender: Any) {

    // Perform updates on certain events
    if sender is NSSlider{
        let event = NSApplication.shared().currentEvent

        if event?.type == NSEventType.leftMouseUp {
            print("LeftMouseUp event inside continuous state NSSlider")
        }
    }

    // Do continuous updates
    if let value = mySlider?.integerValue{
        demoLabel.stringValue = String(value)
    }
}

...
披肩女神 2024-10-04 08:37:48

这只能通过子类化来完成(@mprudhom 等方法对于已知版本不会 100% 工作)

对于拖动的开始,您需要捕获 becomeFirstResponder (为此您还需要提供needsPanelToBecomeKey

为了结束拖动,您需要子类化mouseDown:(它称为mouseDown,但当旋钮释放时它会被调用)

注意:这种方法将使您的滑块第一响应者,取消任何其他当前的第一响应者

注意:来自 mprudhom 的方法

@implementation CustomSlider

- (void)mouseDown:(NSEvent *)theEvent {
    [super mouseDown:theEvent];
    NSLog(@"OK");
}

- (BOOL)needsPanelToBecomeKey {
    [super needsPanelToBecomeKey];
    return YES;
}

- (BOOL)becomeFirstResponder {
    [super becomeFirstResponder];
    NSLog(@"Became first responder.");
    return YES;
}

@end

This can be done only by subclassing (methods like from @mprudhom won't work 100% for know release)

For the beginning of the drag, you need to catch becomeFirstResponder (for this you need to also provide needsPanelToBecomeKey)

for the end of dragging, you need to subclass mouseDown: (its called mouseDown, but it gets called when knob released)

Note: this approach will make your slider the first responder, canceling any other current first responder

Note: approach from mprudhom

@implementation CustomSlider

- (void)mouseDown:(NSEvent *)theEvent {
    [super mouseDown:theEvent];
    NSLog(@"OK");
}

- (BOOL)needsPanelToBecomeKey {
    [super needsPanelToBecomeKey];
    return YES;
}

- (BOOL)becomeFirstResponder {
    [super becomeFirstResponder];
    NSLog(@"Became first responder.");
    return YES;
}

@end
尾戒 2024-10-04 08:37:48

我有类似的需求,但需要 NSTouchBar 上的滑块。我使用了与 Marc Prud'hommeaux 和 Martin Majewski 类似的“快速而肮脏”的方法,只是要检查的事件略有不同。下面是 Swift 代码,可让您跟踪触摸栏上滑块的连续值和最终值。

func sliderValueChanged(_ sender: NSSlider) {
    let doubleValue = sender.doubleValue

    Swift.print("Continuous value: \(doubleValue)")

    // find out if touch event has ended
    let event = NSApplication.shared().currentEvent
    if event?.type == NSEventType.directTouch {
        if let endedTouches = event?.touches(matching: .ended, in: nil) {
            if (endedTouches.count > 0) {
                Swift.print("The final value was: \(doubleValue)")
            }
        }
    }
}

I had a similar need, but for a slider on an NSTouchBar. I used a similar "quick-and dirty" approach as Marc Prud'hommeaux and Martin Majewski, just slightly different events to inspect. Here's the Swift code that allows you to track both the continuous value AND the final value of the slider on a touchbar.

func sliderValueChanged(_ sender: NSSlider) {
    let doubleValue = sender.doubleValue

    Swift.print("Continuous value: \(doubleValue)")

    // find out if touch event has ended
    let event = NSApplication.shared().currentEvent
    if event?.type == NSEventType.directTouch {
        if let endedTouches = event?.touches(matching: .ended, in: nil) {
            if (endedTouches.count > 0) {
                Swift.print("The final value was: \(doubleValue)")
            }
        }
    }
}
一笑百媚生 2024-10-04 08:37:48

我认为对 NSSlider 进行子类化比在目标中添加额外的代码更好。

此方法转发完整的 mouseup 和 mousedown 事件(可能对获取按键修饰符有用),并准备好在需要时处理其他类型的事件。

该滑块存储鼠标按下时的初始值,因此我们可以获取鼠标松开时的初始值和最终值。这对于可撤消的操作非常有用。

class Slider: NSSlider {
    var mouseDownClosure: ((Slider, NSEvent)->Void)?
    var mouseUpClosure: ((Slider, NSEvent)->Void)?

    var initialValue: Double = 0
    
    override func sendAction(_ action: Selector?, to target: Any?) -> Bool {
        let result = super.sendAction(action, to: target)
    
        if let event = NSApplication.shared.currentEvent {
            switch event.type {
            case .leftMouseUp:
                mouseUpClosure?(self, event)
            case .leftMouseDown:
                mouseDownClosure?(self, event)
            default:
                break
            }
        }
        return result
    }

    override func mouseDown(with event: NSEvent) {
        initialValue = self.doubleValue
        super.mouseDown(with: event)
    }
}

I think it is better to subclass the NSSlider than to add extra code in the target.

This method forwards the complete mouseup and mousedown events ( may be useful to get key modifiers ), and is ready to deal with other kind of events if needed.

This slider stores the initial value on mouse down, so we can get the initial and final values in the mouse up. This is very useful for undoable actions.

class Slider: NSSlider {
    var mouseDownClosure: ((Slider, NSEvent)->Void)?
    var mouseUpClosure: ((Slider, NSEvent)->Void)?

    var initialValue: Double = 0
    
    override func sendAction(_ action: Selector?, to target: Any?) -> Bool {
        let result = super.sendAction(action, to: target)
    
        if let event = NSApplication.shared.currentEvent {
            switch event.type {
            case .leftMouseUp:
                mouseUpClosure?(self, event)
            case .leftMouseDown:
                mouseDownClosure?(self, event)
            default:
                break
            }
        }
        return result
    }

    override func mouseDown(with event: NSEvent) {
        initialValue = self.doubleValue
        super.mouseDown(with: event)
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文