Настройка Заголовка Окна В Mac Os X

Добрый день, %username%! Не так давно возникла необходимость настроить заголовок окна вашей программы в Mac OS X. Если iCal.app и Adress Book.app делают это, то почему бы и мне не сделать то же самое?

Настройка заголовка окна в Mac OS X

Первые же ссылки из Google дали мне несколько подсказок, и даже одна тестовая программа (после долгих танцев с бубном) скомпилировала и отобразила свой нестандартный заголовок.

Но для этого требовалось подключить приватные заголовки, изменить их (чтобы они соответствовали новой версии Mac OS X) и т. д. Но хотелось лучше, хотелось сделать проще и даже задать цвет текста заголовка окна (чтобы гармонизировать с новым цветом заголовка).

Отбросив все неудачные примеры, я начал искать зацепки.

И я узнал, что в обычной программе за отрисовку окна отвечает недокументированный класс NSThemeFrame , мы будем с ним работать.

Осторожно! Представлено под катом магия времени выполнения .

Для начала нам понадобится приватный заголовок NSThemeFrame.h (не оригинальный, а перевернутый, конечно), его легко загуглить.

Если ты ленив, то вот прямая ссылка .

Добавлять его в проект не обязательно, он нам нужен только для изучения.

Просмотрев его, обратим внимание на методы drawRect: и _drawTitleStringIn:withColor:.

Названия красноречивы, поэтому мы перегрузим их, чтобы полностью контролировать рендеринг окон.

Вооружившись , Давайте начнем.

Во-первых, нам нужно каким-то образом получить класс NSThemeFrame. Получить его можно из приватной шапки, но это плохой вариант. Допустим в AppDelegate у нас есть выход для нашего NSWindow, тогда чтобы получить нужный класс делаем так:

  
  
   

id _class = [[[self.window contentView] superview] class];

Почему? Потому что NSThemeFrame — это базовый View в окне, а наш contentView уже находится на нем.

Во-вторых, перейдем к магии .

Нам нужно объявить наш класс, он содержит методы drawInRect: и _drawTitleStringIn:withColor:, затем добавить эти методы в класс NSThemeFrame (но под другими именами) и, наконец, поменять местами методы на оригинальные, чтобы иметь возможность чтобы вызвать оригинальные из новых.

Звучит сложно? Ну, время выполнения помогает! Объявим вспомогательный класс DrawHelper (он не будет использоваться напрямую, поэтому не обращаем внимания на предупреждение при компиляции).



#import <objc/runtime.h> // global frame color static NSColor * gFrameColor = nil; // global title color static NSColor * gTitleColor = nil; @interface DrawHelper : NSObject { } // to prevent errors - (float)roundedCornerRadius; - (void)drawRectOriginal:(NSRect)rect; - (void) _drawTitleStringOriginalIn: (NSRect) rect withColor: (NSColor *) color; - (NSWindow*)window; - (id)_displayName; - (NSRect)bounds; - (void)_setTextShadow:(BOOL)on; - (void)drawRect:(NSRect)rect; - (void) _drawTitleStringIn: (NSRect) rect withColor: (NSColor *) color; @end @implementation DrawHelper - (void)drawRect:(NSRect)rect { // Call original drawing method [self drawRectOriginal:rect]; [self _setTextShadow:NO]; NSRect titleRect; NSRect brect = [self bounds]; // creating round-rected bounding path float radius = [self roundedCornerRadius]; NSBezierPath *path = [NSBezierPath alloc]; NSPoint topMid = NSMakePoint(NSMidX(brect), NSMaxY(brect)); NSPoint topLeft = NSMakePoint(NSMinX(brect), NSMaxY(brect)); NSPoint topRight = NSMakePoint(NSMaxX(brect), NSMaxY(brect)); NSPoint bottomRight = NSMakePoint(NSMaxX(brect), NSMinY(brect)); [path moveToPoint: topMid]; [path appendBezierPathWithArcFromPoint: topRight toPoint: bottomRight radius: radius]; [path appendBezierPathWithArcFromPoint: bottomRight toPoint: brect.origin radius: radius]; [path appendBezierPathWithArcFromPoint: brect.origin toPoint: topLeft radius: radius]; [path appendBezierPathWithArcFromPoint: topLeft toPoint: topRight radius: radius]; [path closePath]; [path addClip]; // rect for title titleRect = NSMakeRect(0, 0, brect.size.width, brect.size.height); // get current context CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; // multiply mode - for colorizing original border CGContextSetBlendMode(context, kCGBlendModeMultiply); // draw background if (!gFrameColor) // default bg color gFrameColor = [NSColor colorWithCalibratedRed: (126 / 255.0) green: (161 / 255.0) blue: (177 / 255.0) alpha: 1.0]; [gFrameColor set]; [[NSBezierPath bezierPathWithRect:rect] fill]; // copy mode - for title CGContextSetBlendMode(context, kCGBlendModeCopy); // draw title text [self _drawTitleStringIn: titleRect withColor: nil]; } - (void)_drawTitleStringIn: (NSRect) rect withColor: (NSColor *) color { if (!gTitleColor) // default text color gTitleColor = [NSColor colorWithCalibratedRed: 1.0 green: 1.0 blue: 1.0 alpha: 1.0]; [self _drawTitleStringOriginalIn: rect withColor: gTitleColor]; } @end

Здесь все довольно просто.

Объявляем два цвета - цвет заголовка и цвет текста, объявляем наш класс, он содержит кучу нужных нам методов (их реализовывать не нужно, они есть в NSThemeFrame) и собственно два наших метода для рисования текст и фон.

Для простоты примера я нарисовал стандартный заголовок и «раскрасил» его одним цветом (это позволяет легко сохранить привычный «объем» заголовка).

Вы также можете сделать полностью индивидуальный рендеринг, используя NSImage или градиенты, и вам даже не придется вызывать drawRectOriginal:, потому что тогда нам не понадобится стандартный заголовок.

Но оставим это для самостоятельных упражнений.

После вызова стандартного метода рисования заголовка переходим к созданию нашей области рисования.

Обычно это прямоугольник с закругленными углами.

Реализация для других типов окон (например, с незакругленными нижними углами) также оставляю для самостоятельной работы.

Ну а дальше наш цвет рисуется поверх уже нарисованного стандартного заголовка в режиме умножения (подробнее о режимах можно прочитать в документации Apple).

И в самом конце мы рисуем наш текст заголовка.

Снова вызывается наша функция, которая игнорирует переданный ей цвет и принудительно рисует текст заранее заданным цветом (через исходную функцию рисования).

И вот мы переходим к самому интересному! На самом деле, магия :

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification { id _class = [[[self.window contentView] superview] class]; // Exchange drawRect: Method m0 = class_getInstanceMethod([DrawHelper class], @selector(drawRect:)); class_addMethod(_class, @selector(drawRectOriginal:), method_getImplementation(m0), method_getTypeEncoding(m0)); Method m1 = class_getInstanceMethod(_class, @selector(drawRect:)); Method m2 = class_getInstanceMethod(_class, @selector(drawRectOriginal:)); method_exchangeImplementations(m1, m2); // Exchange _drawTitleStringIn:withColor: Method m3 = class_getInstanceMethod([DrawHelper class], @selector(_drawTitleStringIn:withColor:)); class_addMethod(_class, @selector(_drawTitleStringOriginalIn:withColor:), method_getImplementation(m3), method_getTypeEncoding(m3)); Method m4 = class_getInstanceMethod(_class, @selector(_drawTitleStringIn:withColor:)); Method m5 = class_getInstanceMethod(_class, @selector(_drawTitleStringOriginalIn:withColor:)); method_exchangeImplementations(m4, m5); }

(в моем случае я помещаю этот код в AppDelegate.m, чтобы убедиться, что окно уже создано) Чтобы: 1. получить класс NSThemeFrame 2. берем метод drawRect: из класса DrawHelper 3. добавьте этот метод в класс NSThemeFrame под именем drawRectOriginal: 4. Берем методы drawInRect: и drawRectOriginal из класса NSThemeFrame: 5. поменять местами свои реализации! Далее мы делаем то же самое для метода _drawTitleStringIn:withColor:.

И теперь мы можем радоваться! Наше окно радует (или не очень) глаз нестандартным цветом заголовка.

Если очень хочется сделать какой-то «скиннинг» (изменение цвета заголовка на лету), то класс DrawHelper и содержимое функции applicationWillFinishLaunching: необходимо разместить в отдельном .

m файле, как и функции доступа для gFrameColor и gTitleColor необходимо объявить и реализовать.

И не забудьте перерисовать все окна после изменения этих настроек.

Но, опять же, оставлю это читателю как самостоятельное произведение.

Но, как и следовало ожидать, у такого подхода есть недостатки: 1. для получения класса NSThemeFrame нам понадобится уже созданное окно; 2. этот метод не предполагает отдельной настройки окон, например, нельзя создать два окна с разными заголовками (конечно, можно, но это потребует больших усилий и большого количества кода); 3. окна можно рисовать в обход NSThemeFrame, например, с помощью NSGrayFrame, тогда этот метод скорее всего не поможет, и со вторым классом тоже придется играться; 4. игры с рантаймом хороши в меру.

PS: изначально все это делалось в комбинации Qt+Cocoa, но было перенесено на чистый Cocoa. Если кого-то интересуют тонкости взаимодействия Qt с Cocoa, могу поделиться своим опытом.

PPS: Не вижу смысла выкладывать код на GitHub; его можно очень легко перенести в любой проект, просто скопировав его в AppDelegate.m. Теги: #mac os x #mac os x #objective-c #недокументированные функции #Cocoa #objective-c #Разработка для MacOS

Вместе с данным постом часто просматривают: