14

I've got an NSWindow and an horizontal NSSlider.

I'd like to change the color of the right part of the slider bar when the window background color changes.

enter image description here

enter image description here

Currently, the right part of the bar isn't visible anymore when the window background is dark.

Note: the window background is actually a gradient I'm drawing by overriding drawRect in a subclass of the window's view.

I thought I could change the slider bar fill color by subclassing NSSliderCell and overriding some method like drawBarInside but I don't understand how it works: should I make a little square image and draw it repeatedly in the rect after the knob? Or maybe just draw a colored line? I've never done this before.

I've looked at this question and it's interesting but it's about drawing the knob and I don't need that for now.

I also had a look at this one which seemed very promising, but when I try to mimic this code my slider bar just disappears...

In this question they use drawWithFrame and it looks interesting but again I'm not sure how it works.

I would like to do this with my own code instead of using a library. Could somebody give me a hint about how to do this please? :)

I'm doing this in Swift but I can read/use Objective-C if necessary.

Community
  • 1
  • 1
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
  • I guess you can't access those attributes from the standard interface. The 2nd link looks promising. I guess you should get it running. Basically a slider is a simple view where the knob follows drag-n-drop. – qwerty_so Apr 19 '15 at 12:23

4 Answers4

16

First, I created an image of a slider bar and copied it in my project.

Then I used this image in the drawBarInside method to draw in the bar rect before the normal one, so we'll see only the remainder part (I wanted to keep the blue part intact).

This has to be done in a subclass of NSSliderCell:

class CustomSliderCell: NSSliderCell {

    let bar: NSImage

    required init(coder aDecoder: NSCoder) {
        self.bar = NSImage(named: "bar")!
        super.init(coder: aDecoder)
    }

    override func drawBarInside(aRect: NSRect, flipped: Bool) {
        var rect = aRect
        rect.size = NSSize(width: rect.width, height: 3)
        self.bar.drawInRect(rect)
        super.drawBarInside(rect, flipped: flipped)
    }

}

Pro: it works. :)

Con: it removes the rounded edges of the bar and I haven't found a way to redraw this yet.

custom nsslider

UPDATE:

I made a Swift version of the accepted answer, it works very well:

class CustomSliderCell: NSSliderCell {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func drawBarInside(aRect: NSRect, flipped: Bool) {
        var rect = aRect
        rect.size.height = CGFloat(5)
        let barRadius = CGFloat(2.5)
        let value = CGFloat((self.doubleValue - self.minValue) / (self.maxValue - self.minValue))
        let finalWidth = CGFloat(value * (self.controlView!.frame.size.width - 8))
        var leftRect = rect
        leftRect.size.width = finalWidth
        let bg = NSBezierPath(roundedRect: rect, xRadius: barRadius, yRadius: barRadius)
        NSColor.orangeColor().setFill()
        bg.fill()
        let active = NSBezierPath(roundedRect: leftRect, xRadius: barRadius, yRadius: barRadius)
        NSColor.purpleColor().setFill()
        active.fill()
    }

}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
15

This is correct, you have to subclass the NSSliderCell class to redraw the bar or the knob.

NSRect is just a rectangular container, you have to draw inside this container. I made an example based on an custom NSLevelIndicator that I have in one of my program.

First you need to calculate the position of the knob. You must pay attention to the control minimum and maximum value. Next you draw a NSBezierPath for the background and another for the left part.

Custom NSSlider control bar

#import "MyCustomSlider.h"

@implementation MyCustomSlider

- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {

//  [super drawBarInside:rect flipped:flipped];

    rect.size.height = 5.0;

    // Bar radius
    CGFloat barRadius = 2.5;

    // Knob position depending on control min/max value and current control value.
    CGFloat value = ([self doubleValue]  - [self minValue]) / ([self maxValue] - [self minValue]);

    // Final Left Part Width
    CGFloat finalWidth = value * ([[self controlView] frame].size.width - 8);

    // Left Part Rect
    NSRect leftRect = rect;
    leftRect.size.width = finalWidth;

    NSLog(@"- Current Rect:%@ \n- Value:%f \n- Final Width:%f", NSStringFromRect(rect), value, finalWidth);

    // Draw background track
    NSBezierPath* bg = [NSBezierPath bezierPathWithRoundedRect: rect xRadius: barRadius yRadius: barRadius];
    [NSColor.orangeColor setFill];
    [bg fill];

    // Draw active track
    NSBezierPath* active = [NSBezierPath bezierPathWithRoundedRect: leftRect xRadius: barRadius yRadius: barRadius];
    [NSColor.purpleColor setFill];
    [active fill];


}

@end
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
Atika
  • 1,560
  • 18
  • 18
  • Thanks a lot, it looks very helpful. I will adapt this to Swift and test it as soon as I can. – Eric Aya Apr 23 '15 at 17:37
  • Works like a charm! I'm marking your answer as accepted, and I'm updating mine with the Swift version I made from your example. Thank you ! – Eric Aya Apr 25 '15 at 14:26
  • There's *sometimes* a little blur like [this](https://www.evernote.com/shard/s89/sh/3815fed6-c28a-43ba-a8a5-ebe11c96f90f/47918faa41523aa7cfa4c5a0e962ae35/deep/0/BoomBox.png) on the left of the knob when it moves. Do you think I need to force the UI to refresh somewhere? – Eric Aya Apr 25 '15 at 16:18
  • 1
    If you speak of the effect on the left of the knob, you can adjust the last value (8), normally its the knob width: `CGFloat finalWidth = value * ([[self controlView] frame].size.width - 8);` – Atika Apr 26 '15 at 19:59
  • Thanks, but this seems to have ghosting colors when pulling the circle. – Naoto Ida Sep 18 '15 at 06:46
  • Thanks for the solution! I notice some off-center and added this line: rect.origin.y = rect.origin.y + 1; – Jiulong Zhao Aug 05 '19 at 14:13
  • The ghosting is happening because the given solution overrides aRect height for whatever reason. See improved answer below. – videolist Sep 25 '21 at 16:33
  • @videolist where is the improved solution posted? You mean the solution posted by Morten? – Ananth Kamath Dec 12 '22 at 06:34
5

Swift 5

class CustomSliderCell: NSSliderCell {
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func drawBar(inside aRect: NSRect, flipped: Bool) {
        var rect = aRect
        rect.size.height = CGFloat(5)
        let barRadius = CGFloat(2.5)
        let value = CGFloat((self.doubleValue - self.minValue) / (self.maxValue - self.minValue))
        let finalWidth = CGFloat(value * (self.controlView!.frame.size.width - 8))
        var leftRect = rect
        leftRect.size.width = finalWidth
        let bg = NSBezierPath(roundedRect: rect, xRadius: barRadius, yRadius: barRadius)
        NSColor.orange.setFill()
        bg.fill()
        let active = NSBezierPath(roundedRect: leftRect, xRadius: barRadius, yRadius: barRadius)
        NSColor.purple.setFill()
        active.fill()
    }
}
madx
  • 6,723
  • 4
  • 55
  • 59
Morten J
  • 814
  • 10
  • 21
4

I have achieved this without redraw or override cell at all. Using "False color" filter seems work very well and it is only a few codes!

class RLSlider: NSSlider {
init() {
    super.init(frame: NSZeroRect)
    addFilter()
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
    addFilter()
}

func addFilter() {
    let colorFilter = CIFilter(name: "CIFalseColor")!
    colorFilter.setDefaults()
    colorFilter.setValue(CIColor(cgColor: NSColor.white.cgColor), forKey: "inputColor0")
    colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor1")

//        colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor0")
//        colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor1")

    self.contentFilters = [colorFilter]
}
}

enter image description here

brianLikeApple
  • 4,262
  • 1
  • 27
  • 46