Cocoa 开发中的文本绘制、事件处理与图像视图优化
1. 重构测试
在进行代码重构后,需要确保应用程序仍能正常构建和运行。具体操作步骤如下:
1. 保存所有文件。
2. 构建并重新运行应用程序,此时应用的显示效果应与重构前一致,之后便可开始添加新功能。
2. 文本系统概述
内置的
NSTextField
和
NSTextView
类功能强大,能满足许多常见需求。但在某些情况下,你可能需要自定义文本绘制方式,例如直接在按钮上绘制文本,或者避免创建多个
NSTextView
实例带来的开销。需要注意的是,文本系统是 Cocoa 中较为复杂的部分,涉及 Unicode、书写方向、字距调整等诸多因素,AppKit 会处理这些问题,因此可能需要查看大量方法才能找到所需功能。
3. 字体操作
3.1 NSFont 类
NSFont
类可用于选择不同大小和样式的字体。例如,创建一个 18 磅的 Helvetica 字体对象,代码如下:
NSFont* myFont = [NSFont fontWithName:@"Helvetica" size:18.0];
3.2 NSFontManager 类
NSFontManager
类可简化字体属性的应用。以下是创建 18 磅 Helvetica 粗体和斜体字体的示例:
// 创建 18 磅 Helvetica 粗体
NSFont* myFont = [NSFont fontWithName:@"Helvetica" size:18.0];
NSFontManager* fontManager = [NSFontManager sharedFontManager];
myFont = [fontManager convertFont:myFont toHaveTrait:NSBoldFontMask];
// 创建 18 磅 Helvetica 斜体
NSFont* myFont = [NSFont fontWithName:@"Helvetica" size:18.0];
NSFontManager* fontManager = [NSFontManager sharedFontManager];
myFont = [fontManager convertFont:myFont toHaveTrait:NSItalicFontMask];
3.3 NSFontDescriptor 类
NSFontDescriptor
类提供了更全面的字体控制,但对于简单的粗体或斜体需求,使用
NSFontManager
更为便捷。
4. 属性字符串
4.1 NSAttributedString 类
在 AppKit 中,通常使用
NSAttributedString
来绘制文本。该类并非
NSString
的直接子类,它通过
initWithString:
和
string
方法实现与
NSString
的转换。通过创建属性字典,可以自定义文本的外观。以下是一些常用的属性键:
| 属性键 | 描述 |
| ---- | ---- |
|
NSFontAttributeName
| 字体属性 |
|
NSParagraphStyleAttributeName
| 段落样式属性 |
|
NSForegroundColorAttributeName
| 前景色属性 |
|
NSUnderlineStyleAttributeName
| 下划线样式属性 |
|
NSSuperscriptAttributeName
| 上标属性 |
|
NSBackgroundColorAttributeName
| 背景色属性 |
|
NSUnderlineColorAttributeName
| 下划线颜色属性 |
|
NSStrikethroughStyleAttributeName
| 删除线样式属性 |
|
NSStrikethroughColorAttributeName
| 删除线颜色属性 |
|
NSShadowAttributeName
| 阴影属性 |
|
NSObliquenessAttributeName
| 倾斜度属性 |
以下是设置文本颜色和字体并在视图中绘制的示例代码:
- (void)drawRect:(NSRect)rect {
[[NSColor darkGrayColor] set];
NSRectFill ( self.bounds );
NSString* text = @"All of this text is Times white.";
NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
// 使用白色文本
[attributes setObject: [NSColor whiteColor]
forKey: NSForegroundColorAttributeName];
// 使用 72 磅 Times 字体
[attributes setObject: [NSFont fontWithName: @"Times" size: 72.0]
forKey: NSFontAttributeName];
NSAttributedString* styledText = nil;
styledText = [[NSAttributedString alloc] initWithString: text
attributes: attributes];
[styledText drawInRect:self.bounds];
[styledText release];
}
也可以跳过创建
NSAttributedString
对象的步骤,直接使用属性字典绘制
NSString
:
[[NSColor darkGrayColor] set];
NSRectFill ( self.bounds );
NSString* text = @"All of this text is Times white.";
NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
[attributes setObject: [NSColor whiteColor]
forKey: NSForegroundColorAttributeName];
[attributes setObject: [NSFont fontWithName: @"Times" size: 72.0]
forKey: NSFontAttributeName];
[text drawInRect:self.bounds withAttributes:attributes];
4.2 NSMutableAttributedString 类
NSMutableAttributedString
类允许对字符串的不同部分添加、删除或更改属性。以下是一个示例:
[[NSColor whiteColor] set];
NSRectFill ( self.bounds );
NSString* text = @"Regular Text. Bold Text.";
NSMutableAttributedString* styledText;
styledText = [[NSMutableAttributedString alloc] initWithString:text];
// 创建 48 磅 Helvetica 字体
NSFont* font = [NSFont fontWithName: @"Helvetica" size: 48.0];
// 为整个字符串添加字体属性
[styledText addAttribute: NSFontAttributeName
value: font
range: [text rangeOfString:text]];
// 创建粗体字体
NSFontManager* fontManager = [NSFontManager sharedFontManager];
NSFont* boldFont = [fontManager convertFont:font toHaveTrait:NSBoldFontMask];
// 为部分字符串添加粗体字体属性
[styledText addAttribute: NSFontAttributeName
value: boldFont
range: [text rangeOfString:@"Bold Text."]];
[styledText drawInRect: self.bounds];
[styledText release];
5. 段落样式
NSParagraphStyle
和
NSMutableParagraphStyle
类可用于控制文本块的整体布局,如对齐方式、行间距和缩进等。以下是将文本居中显示的示例:
[[NSColor colorWithDeviceRed: 168.0 / 255.0
green: 128.0 / 255.0
blue: 0.0 / 255.0
alpha: 255.0 / 255.0] set];
NSRectFill ( self.bounds );
NSString* text = @"Centered Text.";
NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
[attributes setObject: [NSFont fontWithName:@"Helvetica" size:85.0]
forKey: NSFontAttributeName];
[attributes setObject: [NSColor yellowColor]
forKey: NSForegroundColorAttributeName];
// 使用段落样式将文本居中
NSMutableParagraphStyle* centerStyle = [[NSMutableParagraphStyle alloc] init];
[centerStyle setAlignment:NSCenterTextAlignment];
[attributes setObject: centerStyle
forKey: NSParagraphStyleAttributeName];
NSRect textRect = NSInsetRect ( self.bounds, 30, 30 );
[text drawInRect: textRect withAttributes: attributes];
[centerStyle release];
6. 文本大小和垂直对齐
NSAttributedString
的
-size
方法可在绘制前计算字符串的大小,有助于调整视图布局。以下是根据字符串高度将文本垂直居中的示例:
[[NSColor blackColor] set];
NSRect viewBounds = self.bounds;
NSRectFill ( viewBounds );
NSString* text = @"Red Orange Yellow";
NSMutableAttributedString* styledText = nil;
styledText = [[NSMutableAttributedString alloc] initWithString:text];
NSString* colorKey = NSForegroundColorAttributeName;
// 为每个单词应用颜色
[styledText addAttribute: colorKey
value: [NSColor redColor]
range: [text rangeOfString:@"Red"]];
[styledText addAttribute: colorKey
value: [NSColor orangeColor]
range: [text rangeOfString:@"Orange"]];
[styledText addAttribute: colorKey
value: [NSColor yellowColor]
range: [text rangeOfString:@"Yellow"]];
// 为整个文本应用 32 磅 Helvetica 字体
[styledText addAttribute: NSFontAttributeName
value: [NSFont fontWithName: @"Helvetica" size: 32.0]
range: [text rangeOfString:text]];
// 水平居中并截断超出部分
NSMutableParagraphStyle* pStyle = [[NSMutableParagraphStyle alloc] init];
pStyle.alignment = NSCenterTextAlignment;
pStyle.lineBreakMode = NSLineBreakByTruncatingTail;
[styledText addAttribute: NSParagraphStyleAttributeName
value: pStyle
range: [text rangeOfString:text]];
[pStyle release];
CGFloat viewWidth = viewBounds.size.width;
CGFloat viewHeight = viewBounds.size.height;
CGFloat textHeight = styledText.size.height;
// 调整文本矩形大小并垂直居中
NSRect textRect;
textRect.size = NSMakeSize ( viewWidth, textHeight );
textRect.origin = NSMakePoint ( 0, (viewHeight - textHeight) * 0.5 );
// 添加边距
textRect = NSInsetRect ( textRect, 30, 0 );
[styledText drawInRect: textRect];
[styledText release];
7. 为 StyledImageView 添加标题
7.1 修改头文件
在
StyledImageView.h
中添加标题属性和绘制标题的方法声明:
@interface StyledImageView : NSView
#pragma mark -
#pragma mark Main Properties
// content.
@property (copy) NSImage* mainImage;
@property (copy) NSString* title;
#pragma mark -
#pragma mark Drawing Methods
// drawing methods for subclasses to override.
- (void) drawBackground;
- (void) drawImage;
- (void) drawImageBorder;
- (void) drawImageSheen;
- (void) drawTitle;
#pragma mark -
#pragma mark Delegate Protocols
// allows another class to calculate the geometry.
@protocol StyleImageViewGeometryDelegate <NSObject>
- (NSRect)imageRectForContentRect:(NSRect)contentRect;
- (NSRect)titleRectForContentRect:(NSRect)contentRect;
@end
7.2 修改实现文件
在
StyledImageView.m
中添加新的私有属性和方法,并实现相关功能:
@interface StyledImageView ()
// private properties.
@property (assign) NSRect contentRect;
@property (readonly) NSRect imageRect;
@property (readonly) NSRect titleRect;
@property (copy) NSDictionary* textAttributes;
@property (retain) NSShadow* imageShadow;
@property (retain) NSGradient* imageSheen;
// private methods.
+ (NSShadow*) defaultImageShadow;
+ (NSGradient*) defaultImageSheen;
- (void) setupTextAttributes;
@end
@implementation StyledImageView
// content.
@synthesize mainImage;
@synthesize title;
@synthesize contentRect;
@synthesize textAttributes;
// colors and gradients.
@synthesize backgroundGradient;
@synthesize backgroundColor;
@synthesize borderColor;
// display options.
@synthesize shouldAddShadow;
@synthesize shouldAddSheen;
@synthesize imageShadow;
@synthesize imageSheen;
// delegate.
@synthesize delegate;
- (id)initWithFrame:(NSRect)frame {
if ( self = [super initWithFrame:frame] ) {
}
return self;
}
- (void) dealloc {
self.mainImage = nil;
self.title = nil;
self.textAttributes = nil;
self.backgroundGradient = nil;
self.backgroundColor = nil;
self.borderColor = nil;
self.imageShadow = nil;
self.imageSheen = nil;
self.delegate = nil;
[super dealloc];
}
- (NSRect) imageRect {
if ( NSEqualRects(self.customImageRect, NSZeroRect) == NO )
return self.customImageRect;
// first, check with the delegate.
if ( [self.delegate respondsToSelector:@selector(imageRectForContentRect:)] )
return [self.delegate imageRectForContentRect:self.contentRect];
NSRect imageRect = self.contentRect;
// if there's a title, make room for it in the view.
if ( self.title ) {
imageRect.size.height -= 34;
imageRect.origin.y += 34;
}
// if delegate didn't retun a value, calculate it.
NSImage* image = self.mainImage;
return [image proportionalRectForTargetRect:imageRect];
}
- (NSRect) titleRect {
// first, check with the delegate.
if ( [self.delegate respondsToSelector:@selector(titleRectForContentRect:)] )
return [self.delegate titleRectForContentRect:self.contentRect];
// use the imageRect as a base value, but resize and reposition
// so that it doesn't overlap the image.
NSRect titleRect = self.imageRect;
titleRect.size.height = 24;
titleRect.origin.y -= 40;
return titleRect;
}
- (void) drawTitle {
if ( self.title == nil) return;
NSRect titleRect = self.titleRect;
NSBezierPath* titleContainer;
titleContainer = [NSBezierPath bezierPathWithRoundedRect: titleRect
xRadius: 8.0
yRadius: 8.0];
[[NSColor colorWithDeviceWhite:0.18 alpha:1.0] set];
[titleContainer fill];
CGFloat imageWidth = self.imageRect.size.width;
CGFloat imageHeight = self.imageRect.size.height;
NSString* formatString = @"%@ - %1.0f x %1.0f";
NSString* titleString = [NSString stringWithFormat: formatString,
self.title,
imageWidth,
imageHeight];
if ( self.textAttributes == nil )
[self setupTextAttributes];
NSAttributedString* drawingString;
drawingString = [[NSAttributedString alloc] initWithString: titleString
attributes: self.textAttributes];
[drawingString drawInRect: NSInsetRect(titleRect, 10, 2)];
[drawingString release];
}
- (void) setupTextAttributes {
NSMutableParagraphStyle * pStyle = [[NSMutableParagraphStyle alloc] init];
[pStyle setAlignment: NSCenterTextAlignment];
// create the font and color.
NSColor* color = [NSColor whiteColor];
NSFont* font = [NSFont boldSystemFontOfSize:[NSFont systemFontSize]];
// combine into an attributes dictionary for an attributed string.
NSMutableDictionary * attrs = [NSMutableDictionary dictionaryWithObjectsAndKeys:
font, NSFontAttributeName,
color, NSForegroundColorAttributeName,
pStyle, NSParagraphStyleAttributeName,
nil];
[pStyle release];
self.textAttributes = attrs;
}
- (void)drawRect:(NSRect)rect {
[self drawBackground];
[self drawImage];
[self drawImageBorder];
[self drawImageSheen];
[self drawTitle];
}
7.3 设置标题
在
BasicCocoaDrawingAppDelegate.m
的
-applicationDidFinishLaunching:
方法中设置图像标题:
imageView.mainImage = [NSImage imageNamed:@"SpaceShuttle"];
imageView.title = @"SpaceShuttle";
imageView.backgroundColor = [NSColor darkGrayColor];
imageView.borderColor = [NSColor whiteColor];
保存所有文件并重新构建应用程序,即可在图像下方看到标题和尺寸信息。
8. 处理鼠标和键盘事件
8.1 事件处理方法
在自定义视图中处理用户交互,常用的方法如下:
-
- (void) mouseDown: (NSEvent *)event;
-
- (void) mouseDragged: (NSEvent *)event;
-
- (void) mouseUp: (NSEvent *)event;
-
- (void) keyDown: (NSEvent *)event;
-
- (BOOL) acceptsFirstResponder;
-
- (BOOL) becomeFirstResponder;
8.2 键盘事件处理
处理键盘事件有两种常见方法:
- 手动检查事件对象中的字符:
- (void) keyDown:(NSEvent *)event {
NSString* characters = [event charactersIgnoringModifiers];
// use 'v' key to go full screen.
if ( [characters isEqualToString:@"v"] ) {
[self enableFullScreenView];
}
}
-
使用
-interpretKeyEvents:方法:
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
}
8.3 鼠标事件处理
处理鼠标点击事件,可获取点击次数和鼠标位置:
- (void) mouseDown:(NSEvent *)event {
NSPoint pointInWindow = event.locationInWindow;
NSPoint pointInView = [self convertPointFromBase:pointInWindow];
if ( event.clickCount > 1 ) {
NSLog(@"double click at: %@", pointInView);
} else {
NSLog(@"single click at: %@", pointInView);
}
}
8.4 为 StyledImageView 添加事件支持
在
StyledImageView.m
中添加新的私有属性和方法,并实现相关功能:
// 添加新的私有属性
@property (assign) NSRect customImageRect;
@property (assign) NSPoint mouseDownPointInImage;
// 添加 synthesize 语句
@synthesize customImageRect;
@synthesize mouseDownPointInImage;
// 在 -initWithFrame: 方法中设置默认值
- (id)initWithFrame:(NSRect)frame {
if ( self = [super initWithFrame:frame] ) {
self.customImageRect = NSZeroRect;
self.mouseDownPointInImage = NSZeroPoint;
}
return self;
}
// 修改 -imageRect 方法
- (NSRect) imageRect {
if ( NSEqualRects(self.customImageRect, NSZeroRect) == NO )
return self.customImageRect;
// first, check with the delegate.
if ( [self.delegate respondsToSelector:@selector(imageRectForContentRect:)] )
return [self.delegate imageRectForContentRect:self.contentRect];
// ...
}
// 实现事件处理方法
#pragma mark -
#pragma mark Events
- (void) mouseDown:(NSEvent *)event {
NSPoint pointInWindow = event.locationInWindow;
NSPoint pointInView = [self convertPointFromBase:pointInWindow];
NSRect newRect = self.customImageRect;
// if the user rect is empty, use the default image rect.
if ( NSEqualRects(newRect, NSZeroRect) )
newRect = self.imageRect;
// make sure.
if ( NSPointInRect(pointInView, newRect) ) {
NSPoint pointInImage = pointInView;
pointInImage.x -= newRect.origin.x;
pointInImage.y -= newRect.origin.y;
self.mouseDownPointInImage = pointInImage;
self.customImageRect = newRect;
}
}
- (void) mouseDragged:(NSEvent *)event {
// don't do anything if mouse is outside of image.
if ( NSEqualPoints(self.mouseDownPointInImage, NSZeroPoint) ) return;
NSPoint pointInWindow = event.locationInWindow;
NSPoint pointInView = [self convertPointFromBase:pointInWindow];
// start with current customImageRect.
NSRect newRect = self.customImageRect;
newRect.origin.x = pointInView.x;
newRect.origin.y = pointInView.y;
newRect.origin.x -= self.mouseDownPointInImage.x;
newRect.origin.y -= self.mouseDownPointInImage.y;
self.customImageRect = newRect;
[self setNeedsDisplay:YES];
}
- (void) mouseUp:(NSEvent *)event {
self.mouseDownPointInImage = NSZeroPoint;
}
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
}
- (void)insertText:(id)string {
// reset if the key pressed was 'r'.
NSString* resetKey = @"r";
if ( [string isEqual:resetKey] ) {
self.customImageRect = NSZeroRect;
[self setNeedsDisplay:YES];
}
}
- (BOOL) acceptsFirstResponder {
return YES;
}
- (BOOL) becomeFirstResponder {
return YES;
}
保存文件并重新运行项目,即可实现拖动图像和按 “r” 键重置图像位置的功能。
通过以上步骤,我们可以在 Cocoa 开发中实现文本绘制、图像标题添加以及鼠标和键盘事件处理等功能,提升应用程序的交互性和用户体验。
8.5 事件处理流程总结
为了更清晰地理解事件处理的流程,我们可以用 mermaid 流程图来展示:
graph LR
A[用户操作] --> B{事件类型}
B -->|鼠标点击| C[mouseDown: 方法]
B -->|鼠标拖动| D[mouseDragged: 方法]
B -->|鼠标释放| E[mouseUp: 方法]
B -->|键盘按键| F[keyDown: 方法]
C --> G{是否在图像区域}
G -->|是| H[记录点击位置]
G -->|否| I[不处理]
D --> J{是否已记录点击位置}
J -->|是| K[更新图像位置]
J -->|否| I
F --> L{处理方式}
L -->|手动检查字符| M[处理特定按键]
L -->|使用 interpretKeyEvents:| N[系统处理快捷键]
K --> O[重绘视图]
M --> P[执行相应操作]
N --> P
8.6 事件处理的注意事项
-
第一响应者
:要确保视图能够接收键盘事件,需要在
acceptsFirstResponder和becomeFirstResponder方法中返回YES。 -
坐标转换
:在处理鼠标事件时,需要将窗口坐标转换为视图坐标,使用
convertPointFromBase:方法。 - 边界检查 :在处理鼠标拖动事件时,需要检查鼠标是否在图像区域内,避免不必要的操作。
9. 综合示例总结
9.1 功能总结
本文介绍了在 Cocoa 开发中实现文本绘制、图像标题添加以及鼠标和键盘事件处理的方法,具体功能如下:
| 功能模块 | 实现方法 |
| ---- | ---- |
| 文本绘制 | 使用
NSAttributedString
和
NSMutableAttributedString
类,结合属性字典和段落样式进行文本绘制。 |
| 图像标题添加 | 在
StyledImageView
中添加标题属性和绘制方法,设置标题并显示在图像下方。 |
| 事件处理 | 实现
mouseDown:
、
mouseDragged:
、
mouseUp:
、
keyDown:
等方法,处理鼠标和键盘事件。 |
9.2 代码结构总结
以下是实现上述功能的主要代码文件和方法:
-
StyledImageView.h
:定义标题属性、绘制方法和代理协议。
-
StyledImageView.m
:实现图像和标题的绘制方法、事件处理方法。
-
BasicCocoaDrawingAppDelegate.m
:设置图像和标题。
9.3 操作步骤总结
为了帮助读者更好地实现这些功能,我们总结了以下操作步骤:
1.
重构测试
:保存文件,构建并重新运行应用程序。
2.
文本绘制
:创建属性字典,使用
NSAttributedString
或
NSMutableAttributedString
绘制文本。
3.
图像标题添加
:在
StyledImageView.h
中添加标题属性和方法声明,在
StyledImageView.m
中实现相关功能,在
BasicCocoaDrawingAppDelegate.m
中设置标题。
4.
事件处理
:在
StyledImageView.m
中添加事件处理方法,实现鼠标和键盘事件的处理。
10. 进一步拓展
10.1 更多文本效果
可以尝试使用更多的属性键,如阴影、下划线、删除线等,来实现更丰富的文本效果。例如,添加阴影效果:
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowColor:[NSColor grayColor]];
[shadow setShadowOffset:NSMakeSize(2, -2)];
[shadow setShadowBlurRadius:3];
[attributes setObject:shadow forKey:NSShadowAttributeName];
10.2 复杂事件处理
可以实现更复杂的事件处理,如多点触摸、手势识别等。例如,使用
UIGestureRecognizer
类来处理手势:
UIPanGestureRecognizer* panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
[self addGestureRecognizer:panGesture];
10.3 性能优化
在处理大量文本或频繁的事件时,可能会出现性能问题。可以通过缓存绘制结果、减少不必要的重绘等方法来优化性能。例如,使用
NSCache
类来缓存绘制的文本:
NSCache* textCache = [[NSCache alloc] init];
NSAttributedString* cachedText = [textCache objectForKey:text];
if (cachedText) {
[cachedText drawInRect:textRect];
} else {
// 绘制文本
NSAttributedString* newText = ...;
[textCache setObject:newText forKey:text];
[newText drawInRect:textRect];
}
11. 总结
通过本文的介绍,我们学习了在 Cocoa 开发中实现文本绘制、图像标题添加以及鼠标和键盘事件处理的方法。这些功能可以提升应用程序的交互性和用户体验。在实际开发中,我们可以根据需求进一步拓展和优化这些功能,实现更复杂的应用。同时,要注意代码的结构和性能优化,确保应用程序的稳定性和流畅性。
希望本文对读者在 Cocoa 开发中有所帮助,让我们一起创造出更优秀的应用程序!
超级会员免费看
8

被折叠的 条评论
为什么被折叠?



