29

In a certain height NSTextField , the layout would be like this if text's size is small

enter image description here

the text is flowing to the top ,how to make the text center like this:

enter image description here

Mil0R3
  • 3,876
  • 4
  • 33
  • 61
  • 2
    Possible duplicate of http://stackoverflow.com/questions/1235219/is-there-a-right-way-to-have-nstextfieldcell-draw-vertically-centered-text, take a look at it! – Vervious Aug 02 '12 at 16:45
  • 2
    @Vervious Thanks, it works, but if the text in NSTextField is selected, it back to top-aligned again. – Mil0R3 Aug 03 '12 at 03:05
  • @Veelian Any luck with this? I get the same thing where when the user enters text it is top-aligned and then when they click on say another field then it jumps back to centered – kdbdallas May 30 '13 at 03:52
  • @Vervious Sorry for not good solution for it. – Mil0R3 May 30 '13 at 08:49

8 Answers8

15
class CustomTextFieldCell: NSTextFieldCell {

    func adjustedFrame(toVerticallyCenterText rect: NSRect) -> NSRect {
        // super would normally draw text at the top of the cell
        var titleRect = super.titleRect(forBounds: rect)

        let minimumHeight = self.cellSize(forBounds: rect).height
        titleRect.origin.y += (titleRect.height - minimumHeight) / 2
        titleRect.size.height = minimumHeight

        return titleRect
    }

    override func edit(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, event: NSEvent?) {
        super.edit(withFrame: adjustedFrame(toVerticallyCenterText: rect), in: controlView, editor: textObj, delegate: delegate, event: event)
    }

    override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
        super.select(withFrame: adjustedFrame(toVerticallyCenterText: rect), in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
    }

    override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
        super.drawInterior(withFrame: adjustedFrame(toVerticallyCenterText: cellFrame), in: controlView)
    }

    override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
        super.draw(withFrame: cellFrame, in: controlView)
    }
}
mkl
  • 90,588
  • 15
  • 125
  • 265
Sayanti Mondal
  • 159
  • 1
  • 4
  • 2
    This is not objective-c – Vassilis May 24 '20 at 10:08
  • On macOS 13.3, the vertically centred text jumps back to default position (upper left corner) on view resize :0 While solution from https://stackoverflow.com/questions/10205088/nstextfield-vertical-alignment works more less fine. – Vlad May 31 '23 at 21:31
8

I'm basically using Alex's answer, with one amendment: We should decrease the height of the titleRect if we increase the y-origin. Otherwise our titleRect will end below the end its superview, possibly hiding any content below the NSTextField.

The .h:

#import <Cocoa/Cocoa.h>

@interface VerticallyCenteredTextFieldCell : NSTextFieldCell

@end

The .m:

#import "VerticallyCenteredTextFieldCell.h"

@implementation VerticallyCenteredTextFieldCell

- (NSRect) titleRectForBounds:(NSRect)frame {

CGFloat stringHeight = self.attributedStringValue.size.height;
NSRect titleRect = [super titleRectForBounds:frame];
CGFloat oldOriginY = frame.origin.y;
titleRect.origin.y = frame.origin.y + (frame.size.height - stringHeight) / 2.0;
titleRect.size.height = titleRect.size.height - (titleRect.origin.y - oldOriginY);
return titleRect;
}

- (void) drawInteriorWithFrame:(NSRect)cFrame inView:(NSView*)cView {
[super drawInteriorWithFrame:[self titleRectForBounds:cFrame] inView:cView];
}

@end

Also, VerticallyCenteredTextFieldCell is a subclass of NSTextFieldCell. If you subclass it, change the class of the NSTextFieldCell in Interface Builder, not the NSTextField (I didn't know this).

enter image description here

pvinis
  • 4,059
  • 5
  • 39
  • 59
Erik
  • 2,138
  • 18
  • 18
6

Just subclass NSTextFieldCell (or swizzle it) ... and implement...

- (NSRect) titleRectForBounds:(NSRect)frame {  

  CGFloat stringHeight       = self.attributedStringValue.size.height;
   NSRect titleRect          = [super titleRectForBounds:frame];
          titleRect.origin.y = frame.origin.y + 
                              (frame.size.height - stringHeight) / 2.0;
   return titleRect;
 }
- (void) drawInteriorWithFrame:(NSRect)cFrame inView:(NSView*)cView {
  [super drawInteriorWithFrame:[self titleRectForBounds:cFrame] inView:cView];
 }
jbrennan
  • 11,943
  • 14
  • 73
  • 115
Alex Gray
  • 16,007
  • 9
  • 96
  • 118
  • 3
    For the record, these are instance methods of `NSTextFieldCell`, not `NSTextField`. I put a category on `NSTextFieldCell` and implemented these methods and it worked quite well. – David Schwartz Feb 12 '14 at 02:53
  • 9
    Swizzle it?? Are you kidding me? That's just plain unethical to suggest something like that to solve a problem like this. – T Blank Dec 11 '14 at 20:46
  • 1
    This code is not working well if the textfield has multiple lines. I have mine with 3 and the last line is simply not showing. I am on 10.10. – Duck Jan 01 '15 at 12:50
  • 1
    @Yar This answer isn't, but it contains a link to a solution that requires the use of method swizzling. – T Blank Jun 07 '15 at 17:51
  • Helped me a lot, thanks! One suggestion: Decrease the height of the titleRect if you increase the y-origin, otherwise titleRect will extend beyond its superview. – Erik Nov 18 '15 at 19:46
4

Adding a little to Erik's and Hejazi's answer, you also need to implement selectWithFrame, here is the solution in Swift 2.2:

class VerticallyCenteredTextFieldCell: NSTextFieldCell {
    override func titleRectForBounds(rect: NSRect) -> NSRect {
        var titleRect = super.titleRectForBounds(rect)

        let minimumHeight = self.cellSizeForBounds(rect).height
        titleRect.origin.y += (titleRect.height - minimumHeight) / 2
        titleRect.size.height = minimumHeight

        return titleRect
    }

    override func drawInteriorWithFrame(cellFrame: NSRect, inView controlView: NSView) {
        super.drawInteriorWithFrame(titleRectForBounds(cellFrame), inView: controlView)
    }

    override func selectWithFrame(aRect: NSRect, inView controlView: NSView, editor textObj: NSText, delegate anObject: AnyObject?, start selStart: Int, length selLength: Int) {
        super.selectWithFrame(titleRectForBounds(aRect), inView: controlView, editor: textObj, delegate: anObject, start: selStart, length: selLength);
    }
}
Shizam
  • 9,627
  • 8
  • 51
  • 82
2

Swift version of Erik Wegener's answer (also modified to work with multiple lines too):

class VerticallyCenteredTextFieldCell : NSTextFieldCell {
    override func titleRect(forBounds rect: NSRect) -> NSRect {
        var titleRect = super.titleRect(forBounds: rect)

        let minimumHeight = self.cellSize(forBounds: rect).height
        titleRect.origin.y += (titleRect.height - minimumHeight) / 2
        titleRect.size.height = minimumHeight

        return titleRect
    }

    override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
        super.drawInterior(withFrame: titleRect(forBounds: cellFrame), in: controlView)
    }
}
Hejazi
  • 16,587
  • 9
  • 52
  • 67
1

My solution is based to calculate font height using NSLayoutManager. This way allow more performance that calculate height using methods of attributed string.

- (NSRect)titleRectForBounds:(NSRect)theRect {
    NSRect titleFrame = [super titleRectForBounds:theRect];

    CGFloat fontHeight = [[[NSLayoutManager alloc] init] defaultLineHeightForFont:self.font];
    if (fontHeight < titleFrame.size.height) {
        titleFrame.origin.y = theRect.origin.y + (theRect.size.height - fontHeight) * 0.5f;
        titleFrame.size.height = fontHeight;
    }
    return titleFrame;
}

- (void) drawInteriorWithFrame:(NSRect)frame inView:(NSView *)view {
    [super drawInteriorWithFrame:[self titleRectForBounds:frame] inView:view];
}
93sauu
  • 3,770
  • 3
  • 27
  • 43
-1

Swift 5.x Update

class PaddedTextFieldCell: NSTextFieldCell {
    let padding = NSEdgeInsets(top: 10, left: 10, bottom: 0, right: 5)
    
    override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
        let paddedFrame = cellFrame.insetBy(dx: padding.left, dy: padding.top)
        super.drawInterior(withFrame: paddedFrame, in: controlView)
    }
}
Young suk
  • 1
  • 2
-1

override NSTextField and NSTextFieldCell is a little annoying, so I just override NSTextField and draw(_ dirtyRect: NSRect) method, here is my code

class CustomLabel: NSTextField { 
    
    public var isVerticalCenter: Bool = false 

    override func draw(_ dirtyRect: NSRect) {
    
        // Drawing code here.
        if isVerticalCenter {
            guard let context = NSGraphicsContext.current?.cgContext else {
                return
            }
            
            context.saveGState()
            defer {
                context.restoreGState()
            }
            
            // draw background if needed 
            let backgroundPath = CGMutablePath()
            let cornerRadius = layer?.cornerRadius ?? 0
            backgroundPath.addRoundedRect(in: bounds, cornerWidth: cornerRadius, cornerHeight: cornerRadius)
            context.addPath(backgroundPath)
            context.setFillColor(backgroundColor?.cgColor ?? .clear)
            context.fillPath()
            
            let attributeString = NSAttributedString(string: stringValue, attributes: [.font: self.font!, .foregroundColor: self.textColor!])
            let stringSize = attributeString.boundingRect(with: self.bounds.size)
            attributeString.draw(at: NSPoint(x: (bounds.width - stringSize.width) / 2, y: (bounds.height - stringSize.height) / 2))
            
        } else {
            super.draw(dirtyRect)
        }
    }
    
}
Mike zhao
  • 1
  • 1