Mac开发绘图基础:NSColor、NSView及相关绘图技术详解
1. NSColor基础
NSColor类功能强大,下面介绍其基础用法。
1.1 基本颜色常量
可以使用以下代码获取基本颜色常量:
NSColor* color1 = [NSColor redColor];
NSColor* color2 = [NSColor greenColor];
NSColor* color3 = [NSColor blueColor];
NSColor* color4 = [NSColor purpleColor];
NSColor* color5 = [NSColor yellowColor];
NSColor* color6 = [NSColor orangeColor];
1.2 透明颜色
使用
clearColor
获取完全透明的颜色:
NSColor* color7 = [NSColor clearColor];
1.3 通过通道设置颜色
可以通过指定红、绿、蓝和透明度通道的值来创建颜色,每个通道的值范围是0.0到1.0:
NSColor* color8 = [NSColor colorWithCalibratedRed: 0.25
green: 0.30
blue: 0.45
alpha: 1.0];
如果参考颜色的RGBA值范围是0到255,可以通过除以255将其转换为0.0到1.0的浮点数:
NSColor* color = [NSColor colorWithCalibratedRed: (122.0/255.0)
green: (224.0/255.0)
blue: (185.0/255.0)
alpha: (128.0/255.0) ];
1.4 设置颜色和绘制
获取颜色后,通常使用
-[NSColor set]
方法设置颜色,然后绘制形状或文本。也可以分别设置描边和填充颜色:
NSColor* color1 = [NSColor redColor];
[color1 set];
NSColor* color2 = [NSColor greenColor];
[color2 setStroke];
NSColor* color3 = [NSColor blueColor];
[color3 setFill];
2. 子类化NSView
创建一个名为“BasicCocoaDrawing”的新Cocoa项目,并将其放在
~/CocoaBook/ch10/
目录下。然后向项目中添加一个Objective - C类,该类是NSView的子类。
在创建类时,第二屏将文件命名为
ShapesAndColorsView.m
,确保勾选“Also create ShapesAndColorsView.h”和Targets部分的“BasicCocoaDrawing”项。同时,要从“Subclass of”下拉菜单中选择NSView,否则视图将无法正常工作。
Xcode会创建一个基本的类实现,如下所示:
#import "ShapesAndColorsView.h"
@implementation ShapesAndColorsView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
}
@end
-initWithFrame:
方法类似于大多数类中的
-init
方法,但它接受一个框架作为视图的初始位置和大小。可以将其格式调整为更熟悉的形式:
- (id)initWithFrame:(NSRect)frame {
if ( self = [super initWithFrame:frame] ) {
}
return self;
}
-drawRect:
方法是进行实际绘图的地方。可以使用
NSRectFill()
函数进行绘制,以下是一个重写的
-drawRect:
方法示例:
- (void)drawRect:(NSRect)dirtyRect {
NSColor* backgroundColor = [NSColor orangeColor];
NSColor* foregroundColor = [NSColor yellowColor];
NSRect bounds = self.bounds;
[backgroundColor set];
NSRectFill ( bounds );
CGFloat insetX = ( NSWidth (bounds) * 0.25 );
CGFloat insetY = ( NSHeight (bounds) * 0.25 );
NSRect shape = NSInsetRect ( bounds, insetX, insetY );
[foregroundColor set];
NSRectFill ( shape );
}
这里通过将视图的边界向内缩进25%,使得绘制的形状大小为视图的一半。
3. 何时进行绘制
作为应用程序开发者,不需要直接告诉视图进行绘制,Cocoa会处理这个过程。只需等待
-drawRect:
方法被调用即可。该方法可能每秒被调用多次,因此要确保其效率。例如,如果有计算对象位置的代码,应该只计算一次并将结果保存为实例变量进行缓存。
如果影响视图的数据发生变化(如对象的位置),更新缓存,然后调用
[myView setNeedsDisplay:YES]
通知Cocoa需要更新。视图系统会收集所有这些请求,并在需要绘制时将它们合并为一次对
-drawRect:
方法的调用。
注意,不要直接调用
-drawRect:
方法,否则可能导致视图绘制不正确或应用程序崩溃。
4. 实例化视图
有两种方法可以将视图添加到窗口中,这里先介绍代码实现的方法。修改
BasicCocoaDrawingAppDelegate.m
文件如下:
#import "BasicCocoaDrawingAppDelegate.h"
#import "ShapesAndColorsView.h"
@implementation BasicCocoaDrawingAppDelegate
@synthesize window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect viewFrame = [self.window.contentView bounds];
ShapesAndColorsView* shapeView;
shapeView = [[ShapesAndColorsView alloc] initWithFrame:viewFrame];
[self.window.contentView addSubview:shapeView];
[shapeView release];
}
@end
确保在文件顶部添加
#import "ShapesAndColorsView.h"
语句以避免编译错误。
5. Bounds和Frames
在上面的代码中,使用了窗口内容视图的边界(bounds)作为
ShapesAndColorsView
的框架(frame)。这是一种让视图填充整个窗口的简单方法,但也可以使用内容视图的框架。不过,使用框架时可能不会得到预期的结果。
视图的框架是相对于其父视图的,因此原点(10, 10)是父视图中的位置,而不是窗口中的位置。通常使用父视图的边界更好,因为其原点通常为(0, 0)。
例如:
NSRect frame = NSMakeRect ( 10, 10, 100, 100 );
NSView* parentView = [[NSView alloc] initWithFrame:frame];
// 这将完全填充父视图,因为原点是0,0
NSView* childView = [[NSView alloc] initWithFrame:parentView.bounds];
[parentView addSubview:childView];
而如果使用父视图的框架:
NSRect frame = NSMakeRect ( 10, 10, 100, 100 );
NSView* parentView = [[NSView alloc] initWithFrame:frame];
// 这将在左侧和底部留下10个像素的空白
NSView* childView = [[NSView alloc] initWithFrame:parentView.frame];
[parentView addSubview:childView];
虽然边界的原点并不总是(0, 0),但在简单情况下这是最常见的行为。
保存文件并运行项目,会在窗口中间看到一个大正方形。但此时如果调整窗口大小,视图不会随之调整。可以通过代码使用NSView的
autoresizingMask
属性来解决这个问题。
6. 在代码中设置调整大小的值
autoresizingMask
属性使用位掩码值,这是一种较低级的C技术,用于将多个“标志”组合成一个值。可以使用按位或运算符(
|
)组合多个值:
NSInteger bitmask = ( FirstValue | SecondValue | ThirdValue );
NSView.h
文件顶部列出了
autoresizingMask
属性的可能值:
enum {
NSViewNotSizable = 0,
NSViewMinXMargin = 1,
NSViewWidthSizable = 2,
NSViewMaxXMargin = 4,
NSViewMinYMargin = 8,
NSViewHeightSizable = 16,
NSViewMaxYMargin = 32
};
这些值的含义如下表所示:
| 值 | 描述 |
| ---- | ---- |
| NSViewNotSizable | 完全不调整大小 |
| NSViewMinXMargin | 左边缘可调整大小,将视图固定在右侧 |
| NSViewMaxXMargin | 右边缘可调整大小,将视图固定在左侧 |
| NSViewMinYMargin | 底部边缘可调整大小,将视图固定在顶部 |
| NSViewMaxYMargin | 顶部边缘可调整大小,将视图固定在底部 |
| NSViewWidthSizable | 视图的宽度随父视图变化 |
| NSViewHeightSizable | 视图的高度随父视图变化 |
在这个例子中,要让视图随窗口调整大小,可以这样做:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect viewFrame = [self.window.contentView bounds];
ShapesAndColorsView* shapeView;
shapeView = [[ShapesAndColorsView alloc] initWithFrame:viewFrame];
[self.window.contentView addSubview:shapeView];
// 随窗口调整大小
NSInteger resizingMask = ( NSViewWidthSizable | NSViewHeightSizable );
[shapeView setAutoresizingMask:resizingMask];
[shapeView release];
}
保存文件并重新运行项目,视图现在会随窗口调整大小。
7. 图形上下文
在Cocoa中学习绘图时,
-[NSColor set]
方法可能会让人困惑。实际上,这里涉及到一个名为图形上下文(graphics context)的概念,它是
NSGraphicsContext
的实例。
图形上下文跟踪影响绘图的设置,包括当前颜色。调用
NSColor
的
-set
方法会将颜色分配给图形上下文。当调用
NSRectFill()
函数时,会使用该颜色进行绘制。
图形上下文是堆叠的,可以使用
+saveGraphicsState
创建当前上下文的副本,进行一些更改,绘制内容,然后使用
+restoreGraphicsState
恢复原始上下文,示例如下:
// 将当前颜色设置为蓝色
[[NSColor blueColor] set];
NSRectFill ( outerRectangle );
[NSGraphicsContext saveGraphicsState];
// 将当前颜色设置为白色
[[NSColor whiteColor] set];
NSRectFill ( innerRectangle );
[NSGraphicsContext restoreGraphicsState];
// 恢复后,当前颜色再次变为蓝色
NSRectFill ( innerRectangle );
要注意,
+saveGraphicsState
和
+restoreGraphicsState
必须成对使用,否则会在控制台看到错误信息,视图可能无法正确绘制。
当视图类的
-drawRect:
方法被调用时,Cocoa会自动为你设置图形上下文。该方法传入的矩形表示视图中需要重绘的无效区域。可以重绘整个视图,但只绘制无效区域可能更高效。不过,确定如何只重绘该区域可能比较困难,在某些情况下,计算所需的时间可能比重绘整个视图还要长,因此这个矩形更像是绘制位置的指导,而不是规则。
8. Bezier路径
只有非常简单的形状可以用矩形描述,而可以使用
NSBezierPath
创建任何想象中的2D形状。Bezier路径是Cocoa矢量绘图系统的核心,它允许在不依赖特定分辨率的情况下进行绘制,例如可以在绘图程序中实现缩放功能。与
NSRect
结构体不同,
NSBezierPath
实例是完整的对象,可以将自己绘制到视图中。
8.1 绘制多边形
可以使用
NSBezierPath
逐点创建多边形,以下是一个示例:
- (void)drawRect:(NSRect)dirtyRect {
[[NSColor blueColor] set];
NSRectFill (self.bounds);
NSRect bounds = self.bounds;
CGFloat width = bounds.size.width;
CGFloat height = bounds.size.height;
NSBezierPath* path = [NSBezierPath bezierPath];
[path moveToPoint: NSMakePoint(width*0.35, height*0.1)];
[path lineToPoint: NSMakePoint(width*0.65, height*0.1)];
[path lineToPoint: NSMakePoint(width*0.65, height*0.6)];
[path lineToPoint: NSMakePoint(width*0.9, height*0.6)];
[path lineToPoint: NSMakePoint(width*0.5, height*0.9)];
[path lineToPoint: NSMakePoint(width*0.1, height*0.6)];
[path lineToPoint: NSMakePoint(width*0.35, height*0.6)];
[path closePath];
[[NSColor whiteColor] set];
[path fill];
}
调用路径的
-fill
方法会在当前图形上下文中绘制内容。
8.2 绘制曲线
NSBezierPath
类有很多方法可以向形状中添加曲线,例如
-curveToPoint:
方法:
- (void)curveToPoint: (NSPoint)endPoint
controlPoint1: (NSPoint)controlPoint1
controlPoint2: (NSPoint)controlPoint2;
控制点控制曲线的形状,类似于引力的作用。以下是一个绘制曲线的示例:
- (void)drawRect:(NSRect)dirtyRect {
[[NSColor colorWithDeviceRed:0.2 green:0.6 blue:0.1 alpha:1.0] set];
NSRectFill(self.bounds);
NSBezierPath * path = [NSBezierPath bezierPath];
path.lineWidth = 6.0;
NSRect bounds = self.bounds;
CGFloat width = NSWidth ( bounds );
CGFloat height = NSHeight ( bounds );
NSPoint startPoint = NSMakePoint ( width * 0.1, height * 0.1 );
NSPoint endPoint = NSMakePoint ( width * 0.9, height * 0.9 );
[path moveToPoint: startPoint];
[path curveToPoint: endPoint
controlPoint1: NSMakePoint ( width * 0.9, height * 0.1 )
controlPoint2: NSMakePoint ( width * 0.1, height * 0.9 )];
[[NSColor colorWithDeviceRed:0.2 green:0.8 blue:0.3 alpha:0.9] set];
[path fill];
[[NSColor colorWithDeviceWhite:1.0 alpha:1.0] set];
[path stroke];
}
8.3 添加圆弧
可以使用以下方法向路径中添加圆弧:
- (void)appendBezierPathWithArcWithCenter: (NSPoint)center
radius: (CGFloat)radius
startAngle: (CGFloat)startAngle
endAngle: (CGFloat)endAngle
clockwise: (BOOL)clockwise;
- (void)appendBezierPathWithArcFromPoint: (NSPoint)point1
toPoint: (NSPoint)point2
radius: (CGFloat)radius;
“from point”方法类似于之前使用的方法,从一个点绘制到另一个点的圆弧;“with center”方法从中心开始,以指定的半径绘制圆。可以绘制圆的部分,以创建饼图样式的形状。以下是一个从中心绘制圆弧的示例:
- (void)drawRect:(NSRect)dirtyRect {
[[NSColor purpleColor] set];
NSRectFill(self.bounds);
NSBezierPath * path = [NSBezierPath bezierPath];
path.lineWidth = 6;
NSPoint origin;
origin.x = NSWidth(self.bounds) * 0.5;
origin.y = NSHeight(self.bounds) * 0.5;
CGFloat radius = NSWidth(self.bounds) *0.25;
[path moveToPoint: origin];
[path appendBezierPathWithArcWithCenter: origin
radius: radius
startAngle: 0
endAngle: 321
clockwise: NO];
[[NSColor magentaColor] set];
[path fill];
[[NSColor whiteColor] set];
[path stroke];
}
9. 图像
作为Mac开发者,有多种图像类型可供选择,最常见的是
NSImage
和
CGImage
(正式名称为
CGImageRef
)。在Snow Leopard之后,
NSImage
的底层由
CGImage
支持,因此在两者之间切换变得更加容易。虽然不能直接进行类型转换,但可以使用
NSImage
的以下两个方法:
- (id)initWithCGImage: (CGImageRef)cgImage
size: (NSSize)size;
- (CGImageRef)CGImageForProposedRect: (NSRect *)rect
context: (NSGraphicsContext *)context
hints: (NSDictionary *)hints;
虽然
CGImage
看起来可能是主要类型,但它不是Objective - C类,而是一个不透明的结构体类型,不能为其添加类别或使用Cocoa常用的通用编程技术。因此,建议使用更灵活的
NSImage
类,它具有Objective - C的所有优点,并且一些标准的Cocoa视图类已经知道如何显示
NSImage
,无需进行转换。如果使用垃圾回收,
NSImage
也能直接工作。唯一的缺点是
NSImage
在iPhone上不存在,但有一个大致等效的类
UIImage
。
9.1 加载图像数据
加载现有图像数据主要有两种方式:读取现有图像文件的内容,或从Cocoa请求标准图标。如果使用标准图标,可以使用与Mac OS X本身相同的图标,例如颜色选择器或计算机图标。如果从文件加载自定义图像,通常需要将其复制到项目中,因为无法保证用户机器上有哪些文件。
9.2 从项目中加载图像
首先,找到要使用的图像,几乎任何图像都可以,如PNG或JPEG文件。可以从网站下载或使用照片库中的图像,建议使用大小约为1000×1000像素的图像。将文件从Finder拖到项目的Resources文件夹中,当Xcode询问时,确认要复制文件并将其添加到目标中。可以使用之前“子类化NSView”部分的项目作为起点。
通过以上内容,我们详细介绍了Mac开发中NSColor、NSView、Bezier路径以及图像相关的绘图基础和技术,希望能帮助开发者更好地进行自定义视图和绘图开发。
Mac开发绘图基础:NSColor、NSView及相关绘图技术详解
10. 图像操作总结与对比
为了更清晰地了解
NSImage
和
CGImage
的特点,这里对它们进行一个总结对比,如下表所示:
| 特性 | NSImage | CGImage |
| ---- | ---- | ---- |
| 类型 | Objective - C类 | 不透明结构体类型 |
| 编程灵活性 | 可添加类别,使用通用编程技术 | 需作为数据容器,使用C函数操作 |
| 与Cocoa视图兼容性 | 部分标准Cocoa视图类可直接显示 | 需转换才能使用 |
| 垃圾回收支持 | 直接支持 | 无特殊支持 |
| 平台适用性 | Mac可用,iPhone无 | 无平台限制,但需适配 |
从这个表格可以看出,
NSImage
在大多数情况下更适合Mac开发,尤其是在需要使用Cocoa特性和简化开发流程时。
11. 绘图性能优化
在绘图过程中,性能是一个重要的考虑因素。以下是一些绘图性能优化的建议:
-
缓存计算结果
:如前面提到的,如果有计算对象位置等操作,应该只计算一次并将结果保存为实例变量进行缓存。例如,在
-drawRect:
方法中,如果需要计算某个图形的位置,可以将计算结果保存下来,避免每次调用
-drawRect:
都进行重复计算。
@interface MyView : NSView {
NSRect cachedShapeRect;
BOOL isCacheValid;
}
@end
@implementation MyView
- (void)calculateShapeRect {
// 进行复杂的计算
NSRect bounds = self.bounds;
CGFloat insetX = ( NSWidth (bounds) * 0.25 );
CGFloat insetY = ( NSHeight (bounds) * 0.25 );
cachedShapeRect = NSInsetRect ( bounds, insetX, insetY );
isCacheValid = YES;
}
- (void)drawRect:(NSRect)dirtyRect {
if (!isCacheValid) {
[self calculateShapeRect];
}
// 使用缓存的结果进行绘图
NSColor* foregroundColor = [NSColor yellowColor];
[foregroundColor set];
NSRectFill ( cachedShapeRect );
}
@end
-
只绘制需要更新的区域
:虽然计算只绘制无效区域可能比较困难,但在某些情况下可以显著提高性能。可以通过判断
dirtyRect与需要绘制的图形的交集,只绘制交集部分。
- (void)drawRect:(NSRect)dirtyRect {
NSRect shapeRect = [self calculateShapeRect];
NSRect intersectionRect = NSIntersectionRect(dirtyRect, shapeRect);
if (!NSIsEmptyRect(intersectionRect)) {
// 只绘制交集部分
NSColor* foregroundColor = [NSColor yellowColor];
[foregroundColor set];
NSRectFill ( intersectionRect );
}
}
-
合理使用图形上下文
:确保
+saveGraphicsState和+restoreGraphicsState成对使用,避免不必要的上下文切换和资源浪费。
12. 绘图流程总结
为了更清晰地展示整个绘图过程,下面是一个mermaid格式的流程图:
graph TD;
A[创建NSView子类] --> B[重写-initWithFrame:方法];
B --> C[重写-drawRect:方法];
C --> D[设置图形上下文和颜色];
D --> E[绘制基本形状或Bezier路径];
E --> F[判断是否需要更新绘图数据];
F -- 是 --> G[更新缓存数据];
G --> H[调用setNeedsDisplay:YES];
F -- 否 --> I[等待下次-drawRect:调用];
H --> I;
I --> C;
这个流程图展示了从创建视图子类到进行绘图,再到处理数据更新的整个过程。
13. 常见绘图错误及解决方法
在绘图过程中,可能会遇到一些常见的错误,以下是一些常见错误及解决方法:
| 错误现象 | 可能原因 | 解决方法 |
| ---- | ---- | ---- |
| 视图不显示 | 视图未正确添加到窗口,或
-drawRect:
方法未正确实现 | 检查视图添加代码,确保
-drawRect:
方法中有正确的绘图代码 |
| 颜色显示不正确 | 颜色设置错误,或图形上下文未正确管理 | 检查颜色设置代码,确保
+saveGraphicsState
和
+restoreGraphicsState
成对使用 |
| 视图绘制闪烁 | 频繁调用
setNeedsDisplay:YES
,或
-drawRect:
方法效率低下 | 优化数据更新逻辑,减少
setNeedsDisplay:YES
的调用次数,优化
-drawRect:
方法中的计算代码 |
| 应用程序崩溃 | 内存管理问题,或直接调用
-drawRect:
方法 | 检查内存管理代码,避免直接调用
-drawRect:
方法 |
14. 扩展应用:自定义绘图组件
基于前面介绍的绘图技术,可以创建自定义的绘图组件。例如,创建一个自定义的按钮组件,该按钮可以有自定义的形状和颜色。
// CustomButtonView.h
#import <Cocoa/Cocoa.h>
@interface CustomButtonView : NSView
@property (nonatomic, strong) NSColor *buttonColor;
@end
// CustomButtonView.m
#import "CustomButtonView.h"
@implementation CustomButtonView
- (instancetype)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.buttonColor = [NSColor blueColor];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[self.buttonColor set];
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:10 yRadius:10];
[path fill];
}
@end
在这个例子中,创建了一个自定义的按钮视图,它可以有自定义的颜色和圆角。可以在其他视图中使用这个自定义按钮视图,如下所示:
// 在某个视图控制器中使用自定义按钮
- (void)viewDidLoad {
[super viewDidLoad];
NSRect buttonFrame = NSMakeRect(100, 100, 200, 50);
CustomButtonView *customButton = [[CustomButtonView alloc] initWithFrame:buttonFrame];
[self.view addSubview:customButton];
}
15. 总结与展望
通过本文的介绍,我们详细了解了Mac开发中绘图的基础知识,包括
NSColor
的使用、
NSView
的子类化、Bezier路径的绘制、图像的加载和处理等。这些知识为开发者进行自定义视图和绘图开发提供了坚实的基础。
在未来的开发中,可以进一步探索更复杂的绘图技术,如动画效果的实现、3D绘图等。同时,随着技术的不断发展,绘图框架也可能会有新的特性和优化,开发者需要不断学习和跟进,以提高自己的开发水平。希望本文能帮助开发者更好地掌握Mac开发中的绘图技术,创造出更美观、高效的应用程序。
超级会员免费看
10

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



