Mac OS Xでのウィンドウタイトルのカスタマイズ

こんにちは、%ユーザー名%!



少し前まで、Mac OS Xでプログラムのウィンドウタイトルをカスタマイズする必要がありました。iCal.appとAdress Book.appでこれを行う場合、同じことをしないのはなぜですか。



Googleからの最初のリンクは、私にいくつかのリードを与え、1つのテストプログラム(タンバリンとの長いダンスの後)でさえ、そのカスタムタイトルをコンパイルして表示しました。 しかし、それはプライベートヘッダーの接続、それらの変更(Mac OS Xの新しいバージョンに一致するため)などを必要としました... 失敗したすべての例を破棄して、私はリードを掘り始めました...



そして、通常のプログラムでは、文書化されていないクラスNSThemeFrameがウィンドウのレンダリングを担当することがわかりました。



ご注意 katの下には実行時の魔法があります



まず、プライベートヘッダーNSThemeFrame.h(オリジナルではなく、もちろん逆)が必要です。Googleで簡単に検索できます。 怠lazなら、 ここに直接リンクがあります。 プロジェクトに追加する必要はありません。学習のためだけに必要です。



ひと目見て、drawRect:および_drawTitleStringIn:withColor:メソッドに注目します。 名前は話しているので、ウィンドウのレンダリングを完全に制御するためにそれらをオーバーロードします。 <objc / runtime.h>を準備して、始めます。



まず、NSThemeFrameクラスを何らかの方法で取得する必要があります。 プライベートヘッダーから取得できますが、これは悪いオプションです。 AppDelegateにNSWindowのアウトレットがあるとし、目的のクラスを取得するには、次のようにします。



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





なんで? NSThemeFrameはウィンドウのベースビューであり、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
      
      





ここではすべてが非常に簡単です。 タイトル色とテキスト色の2つの色を宣言し、クラスを宣言します。クラスには、必要なメソッド(NSThemeFrame内に実装する必要はありません)と、テキストと背景をレンダリングするための2つのメソッドが含まれています。



例を簡単にするために、標準の見出しを描画し、1色で「色付け」します(これにより、見出しの通常の「バルク」を保持する簡単な方法が可能になります)。 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. NSThemeFrameクラスからdrawInRect:およびdrawRectOriginalメソッドを取得します。

5.実装を交換します!



次に、_drawTitleStringIn:withColor:メソッドについても同じことを行います。



そして今、私たちは喜びます! 私たちのウィンドウは、カスタムタイトルの色で私たちの目を喜ばせます(または喜ばせません)。



何らかの「スキニング」(タイトルの色をオンザフライで変更する)を本当に行いたい場合は、DrawHelperクラスとapplicationWillFinishLaunching関数のコンテンツ:別の.mファイルに配置し、gFrameColorとgTitleColorへのアクセス関数を宣言して実装する必要があります。 これらの設定を変更した後、すべてのウィンドウを再描画することを忘れないでください。 しかし、これもまた、読者を独立した作品として残すでしょう。



しかし、予想されるように、このアプローチには欠点があります。



1. NSThemeFrameクラスを取得するには、作成済みのウィンドウが必要です。

2.この方法は、ウィンドウの個別のカスタマイズを意味しません。たとえば、タイトルが異なる2つのウィンドウを作成することはできません(もちろんできますが、多くの労力と多くのコードが必要になります)。

3. NSThemeFrameをバイパスして、たとえばNSGrayFrameを使用してウィンドウを描画できますが、このメソッドはほとんど役に立たないため、2番目のクラスでもプレイする必要があります。

4.ランタイム付きのゲームは、適度に優れています。



PS:最初はこれはすべてQt + Cocoaバンドルで行われていましたが、純粋なCocoaに移植されました。 QtのCocoaとのやり取りのトリックに興味がある人は、その経験を共有できます。



PPS:githubにコードをアップロードする意味がわかりません。AppDelegate.mで簡単なコピーと貼り付けを行うだけで、プロジェクトに簡単に転送できます。



All Articles