实现将NSImage保持指定像素的拉伸

本文介绍如何在AppKit中实现类似UIKit中UIImage的图片拉伸功能。提供了两种自定义方法来保持图片边缘区域不变,同时将图片扩展到指定大小。

转自:http://www.tanhao.me/pieces/1408.html/


在UIKit中的UIImage有以下两个方法可以实现对图片的部分拉伸:

- (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets

但在AppKit中的NSImage却没有类似的方法,于是手动写了类似这两个功能的函数:

//保持四周一定区域像素不拉伸,将图像扩散到一定的大小
- (NSImage *)stretchableImageWithSize:(NSSize)size edgeInsets:(NSEdgeInsets)insets;
//保持leftWidth,rightWidth这左右一定区域不拉伸,将图片宽度拉伸到(leftWidth+middleWidth+rightWidth)
- (NSImage *)stretchableImageWithLeftCapWidth:(float)leftWidth middleWidth:(float)middleWidth rightCapWidth:(float)rightWidth;

详细代码如下:

/**
 * //保持四周一定区域像素不拉伸,将图像扩散到一定的大小
 */
- (NSImage *)stretchableImageWithSize:(NSSize)size edgeInsets:(NSEdgeInsets)insets
{
    void (^makeAreas)(NSRect, NSRect *, NSRect *, NSRect *, NSRect *, NSRect *, NSRect *, NSRect *, NSRect *, NSRect *) = ^(NSRect srcRect, NSRect *tl, NSRect *tc, NSRect *tr, NSRect *ml, NSRect *mc, NSRect *mr, NSRect *bl, NSRect *bc, NSRect *br) {
        CGFloat w = NSWidth(srcRect);//60
        CGFloat h = NSHeight(srcRect);//62
		
        CGFloat cw = w - insets.left - insets.right;//60 - 15 - 15 = 30
        CGFloat ch = h - insets.top - insets.bottom; //62 - 20 -20 = 22
        
        CGFloat x0 = NSMinX(srcRect);//0
        CGFloat x1 = x0 + insets.left;//15
        CGFloat x2 = NSMaxX(srcRect) - insets.right;//   60 - 15 = 45
        
        CGFloat y0 = NSMinY(srcRect);//0
        CGFloat y1 = y0 + insets.bottom;//20
        CGFloat y2 = NSMaxY(srcRect) - insets.top;//62-20=42
        
        *tl = NSMakeRect(x0, y2, insets.left, insets.top);
        *tc = NSMakeRect(x1, y2, cw, insets.top);
        *tr = NSMakeRect(x2, y2, insets.right, insets.top);
        
        *ml = NSMakeRect(x0, y1, insets.left, ch);
        *mc = NSMakeRect(x1, y1, cw, ch);
        *mr = NSMakeRect(x2, y1, insets.right, ch);
        
        *bl = NSMakeRect(x0, y0, insets.left, insets.bottom);
        *bc = NSMakeRect(x1, y0, cw, insets.bottom);
        *br = NSMakeRect(x2, y0, insets.right, insets.bottom);
    };
    
    NSRect rect = NSMakeRect(0, 0, size.width, size.height);
    
    // Source rects
    NSRect srcRect = (NSRect){NSZeroPoint, self.size};
    NSRect srcTopL, srcTopC, srcTopR, srcMidL, srcMidC, srcMidR, srcBotL, srcBotC, srcBotR;
    makeAreas(srcRect, &srcTopL, &srcTopC, &srcTopR, &srcMidL, &srcMidC, &srcMidR, &srcBotL, &srcBotC, &srcBotR);
    
    // Destinations rects
    NSRect dstTopL, dstTopC, dstTopR, dstMidL, dstMidC, dstMidR, dstBotL, dstBotC, dstBotR;
    makeAreas(rect, &dstTopL, &dstTopC, &dstTopR, &dstMidL, &dstMidC, &dstMidR, &dstBotL, &dstBotC, &dstBotR);
    
    NSRect srcRects[] = {srcTopL, srcTopC, srcTopR, srcMidL, srcMidC, srcMidR, srcBotL, srcBotC, srcBotR};
    NSRect dstRects[] = {dstTopL, dstTopC, dstTopR, dstMidL, dstMidC, dstMidR, dstBotL, dstBotC, dstBotR};
    NSMutableArray *partImgs = [NSMutableArray arrayWithCapacity:9];
    for (int i=0;i<9;i++)
    {
        NSRect aSrcRect = srcRects[i];
        NSRect aDstRect = dstRects[i];
        
        NSImage *partImg = [[NSImage alloc] initWithSize:aSrcRect.size];
        [partImg lockFocus];
        [self drawAtPoint:NSZeroPoint fromRect:aSrcRect operation:NSCompositeSourceOver fraction:1.0];
        [partImg setSize:aDstRect.size];
        [partImg unlockFocus];
        [partImgs addObject:partImg];
    }
    
    // Draw
    NSImage *resultImg = [[NSImage alloc] initWithSize:rect.size];
    [resultImg lockFocus];
    NSDrawNinePartImage(rect,
                        [partImgs objectAtIndex:0],
                        [partImgs objectAtIndex:1],
                        [partImgs objectAtIndex:2],
                        [partImgs objectAtIndex:3],
                        [partImgs objectAtIndex:4],
                        [partImgs objectAtIndex:5],
                        [partImgs objectAtIndex:6],
                        [partImgs objectAtIndex:7],
                        [partImgs objectAtIndex:8],
                        NSCompositeSourceOver, 1, NO);
    [resultImg unlockFocus];
    return resultImg;
}


/**
 * //保持leftWidth,rightWidth这左右一定区域不拉伸,将图片宽度拉伸到(leftWidth+middleWidth+rightWidth)
 */
- (NSImage *)stretchableImageWithLeftCapWidth:(float)leftWidth middleWidth:(float)middleWidth rightCapWidth:(float)rightWidth
{
    // Calculate the new images dimensions
    float imageWidth = leftWidth + middleWidth + rightWidth;
    float imageHeight = self.size.height;
    
    // Generate the left image
    NSRect rectLeft = NSMakeRect(0, 0, leftWidth, imageHeight);
    NSImage *imageLeft = [[NSImage alloc] initWithSize:rectLeft.size];
    if (imageLeft.size.width > 0) {
        [imageLeft lockFocus];
        [self drawInRect:rectLeft fromRect:rectLeft operation:NSCompositeCopy fraction:1.0];
        [imageLeft unlockFocus];
    }
    
    // Generate the middle image
    NSRect rectMiddle = NSMakeRect(0, 0, middleWidth, imageHeight);
    NSImage *imageMiddle = [[NSImage alloc] initWithSize:rectMiddle.size];
    if (imageMiddle.size.width > 0) {
        [imageMiddle lockFocus];
        [self drawInRect:rectMiddle fromRect:NSMakeRect(leftWidth, 0, self.size.width-rightWidth-leftWidth,imageHeight) operation:NSCompositeCopy fraction:1.0];
        [imageMiddle unlockFocus];
    }
    
    // Generate the right image
    NSRect rectRight = NSMakeRect(0, 0, rightWidth, imageHeight);
    NSImage *imageRight = [[NSImage alloc] initWithSize:rectRight.size];
    if (imageRight.size.width > 0) {
        [imageRight lockFocus];
        [self drawInRect:rectRight fromRect:NSMakeRect(self.size.width - rightWidth, 0, rightWidth, imageHeight) operation:NSCompositeCopy fraction:1.0];
        [imageRight unlockFocus];
    }
    
    // Combine the images
    NSImage *newImage = [[NSImage alloc] initWithSize:NSMakeSize(imageWidth,  imageHeight)];
    if (newImage.size.width > 0) {
        [newImage lockFocus];
        NSDrawThreePartImage(NSMakeRect(0, 0, imageWidth, imageHeight), imageLeft, imageMiddle, imageRight, NO, NSCompositeSourceOver, 1, NO);
        [newImage unlockFocus];
    }
    
    // Release the images and return the new image
    return newImage;
}



为什么我的子层级没有展开:// // EventCenterViewController.swift // OmadaSurveillance // // Created by 代小青 on 2025/9/19. // import Cocoa import TPBMDesignKit enum PageType { case events case customAlerts case deviceAlerts case none } // MARK: - eventcenter主控制器 class TPGuardEventCenterController: TPHomeBaseViewController{ //侧边栏controller private let eventCenterSidebarController = TPGuardSidebarController() private let eventCenterSplitView: TPGuardEventCenterSpliteView = TPGuardEventCenterSpliteView() private var isIniteventCenterPlayerSplitView: Bool = false // 侧边栏是否已收起 private var isSidebarCollapsed :Bool = false // 右侧三个子页面controller private let eventController = TPGuardEventController() private let customAlertController = TPGuardCustomAlertController() private let deviceAlertController = TPGuardDeviceAlertController() //侧边栏的导航栏 private let eventCenterSidebarScrollView : NSScrollView = NSScrollView() private let eventCenterSidebarOutlineView : TPGuardOutlineView = TPGuardOutlineView() //侧边栏收起按钮 private let sidebarButton : TPBButton = TPBButton() //侧边栏的item private var sidebarItems: [SidebarItem] = [] //第一次加载进内存时候 override func viewDidLoad() { super.viewDidLoad() // Do view setup here. //默认子节点展开等 } override func setupSubviews() { super.setupSubviews() // MARK: - 主界面splitView分割 /*split添加调用 addArrangedSubview 不仅添加了视,还让 NSSplitView 知道:“这个视是我要管理的一个面板”,然后根据 arrangedSubviews 的顺序来决定左右或上下结构*/ eventCenterSplitView.addArrangedSubview(eventCenterSidebarController.view) eventCenterSplitView.addArrangedSubview(eventController.view) //设置代理 eventCenterSplitView.delegate = self //分割线设置+风格 eventCenterSplitView.isVertical = true eventCenterSplitView.dividerStyle = .thin // 异步设置初始宽度(确保布局已完成) DispatchQueue.main.async { [weak self] in guard let self = self else { return } let initialWidth: CGFloat = self.isSidebarCollapsed ? 48 : 160 self.eventCenterSplitView.setPosition(initialWidth, ofDividerAt: 0) // 根据目标宽度设置状态 self.isSidebarCollapsed = false } // 将 splitView 添加到主视 view.addSubview(eventCenterSplitView) // MARK: - 侧边栏层级+按钮设置 // 添加收起按钮、配置 TPBButton 样式 sidebarButton.iconImage = .eventCenterSidebarButton sidebarButton.fillColor = .clear //代理 sidebarButton.target = self sidebarButton.action = #selector(hideSidebar) //将侧边栏按钮添加到侧边栏视 eventCenterSidebarController.view.addSubview(sidebarButton) //将 eventCenterSidebarOutlineView 设置为滚动视的“文档内容视”(即可滚动区域的内容) eventCenterSidebarScrollView.documentView = eventCenterSidebarOutlineView //设置滚动视背景颜色为深灰色(接近黑色) eventCenterSidebarScrollView.backgroundColor = NSColor(hexString: "0x1A1A1A") //关闭系统自动调整内容边距(content insets) eventCenterSidebarScrollView.automaticallyAdjustsContentInsets = false //启用 Auto Layout 布局方式、允许使用约束(constraints)来定位该视 eventCenterSidebarScrollView.translatesAutoresizingMaskIntoConstraints = false //documentView添加水平方向约束让documentView的左右边缘与scrollView 对齐 eventCenterSidebarScrollView.documentView?.snp.remakeConstraints({ make in make.leading.trailing.equalTo(eventCenterSidebarScrollView) }) eventCenterSidebarOutlineView.backgroundColor = NSColor(hexString: "0x1A1A1A") //delegate 管“怎么展示”,dataSource 管“展示什么”。 eventCenterSidebarOutlineView.delegate = self eventCenterSidebarOutlineView.dataSource = self eventCenterSidebarOutlineView.tpGuardDelegate = self //启用自动行高(Auto Sizing Rows)。 eventCenterSidebarOutlineView.usesAutomaticRowHeights = true //禁用列的自动调整大小行为。默认情况下,AppKit 会尝试平均分配或按内容调整列宽。 eventCenterSidebarOutlineView.columnAutoresizingStyle = .noColumnAutoresizing //关闭选中项的高亮样式。原生默认是蓝色或灰色背景高亮。 eventCenterSidebarOutlineView.selectionHighlightStyle = .none //移除表头(Header View)。默认 NSTableView 顶部有一栏用于显示列标题。 eventCenterSidebarOutlineView.headerView = nil //禁用“轮廓列”(Outline Column)的自动放。在 NSOutlineView 中,“outline column”是显示层级结构的那一列(带展开箭头的那列)。 eventCenterSidebarOutlineView.autoresizesOutlineColumn = false //允许子视(cell 内部的 subviews)随容器自动调整大小。这会影响单元格内部控件是否跟随表格宽度拉伸。 eventCenterSidebarOutlineView.autoresizesSubviews = true //启用 Core Animation Layer(即开启层支持)。 eventCenterSidebarOutlineView.wantsLayer = true eventCenterSidebarOutlineView.layer?.borderColor = NSColor(hexString: "0xFFFFFF").withAlphaComponent(0.1).cgColor eventCenterSidebarOutlineView.layer?.borderWidth = 1.0 //注册支持接收某种拖拽类型的数据。 eventCenterSidebarOutlineView.registerForDraggedTypes([.dragOutlineItem]) //设置当前视为拖拽源(source)时允许的操作类型。 eventCenterSidebarOutlineView.setDraggingSourceOperationMask(NSDragOperation.every, forLocal: true) //设置单元格之间的间距 eventCenterSidebarOutlineView.intercellSpacing = NSSize(width: view.bounds.width, height: 30.0) //创建并添加一列到表格中。 let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Column")) eventCenterSidebarOutlineView.addTableColumn(column) // eventCenterSidebarOutlineView.onDoubleClick = { [weak self] item in // if let device = item as? TPGuardDeviceItem, let self { // if device.deviceType == .IPC { // self.selectDevice(for: device) // } // } // } //允许“轮廓列”(Outline Column)根据父视的大小变化自动调整宽度。 eventCenterSidebarOutlineView.autoresizesOutlineColumn = true //设置每一级子节点相对于父节点的像素值(indentation),这里设为 0 像素。 eventCenterSidebarOutlineView.indentationPerLevel = 0 //将scrollView添加到侧边栏视 eventCenterSidebarController.view.addSubview(eventCenterSidebarScrollView) } //添加约束 override func makeConstraints() { super.makeConstraints() // 让 eventCenterPlayerSplitView 的 上边、左边、下边、右边 都等于父视 view 的对应边缘,也就是让这个 splitView 填满整个父视的客户区(不包括窗口标题栏等系统区域)。 eventCenterSplitView.snp.makeConstraints { make in make.top.leading.bottom.trailing.equalTo(view) } // 添加侧边栏按钮的约束 sidebarButton.snp.makeConstraints { make in make.leading.equalToSuperview().offset(15) make.top.equalToSuperview().offset(15) make.size.equalTo(CGSize(width: 18, height: 18)) } //scrollview添加约束(与侧边栏按钮拉开了距离) eventCenterSidebarScrollView.snp.makeConstraints { make in make.top.equalTo(sidebarButton.snp.bottom).offset(8) make.leading.trailing.bottom.equalTo(view) } } // MARK: - 初始化数据 override func setupInitialData() { super.setupInitialData() // 全部事件 let events = SidebarItem(title: "Event", iconImage: .sidebarChooseButton, showsBadge: false) // 自定义告警 let customAlerts = SidebarItem( title: "Custom Alert", iconImage: .sidebarChooseButton, children: [ SidebarItem(title: "alert name", iconImage: .sidebarChooseButton, showsBadge: true, unreadCount: 2), SidebarItem(title: "alert name", iconImage: .sidebarChooseButton, showsBadge: true, unreadCount: 1), SidebarItem(title: "alert name", iconImage: .sidebarChooseButton, showsBadge: true, unreadCount: 2) ], showsBadge: true, unreadCount: 99 ) // 设备告警 let deviceAlerts = SidebarItem(title: "Device Alert", iconImage: .sidebarChooseButton, showsBadge: true, unreadCount: 3) sidebarItems = [events, customAlerts, deviceAlerts] eventCenterSidebarOutlineView.reloadData() eventCenterSidebarOutlineView.expandItem(nil, expandChildren: true) } //绑定侧边栏的按钮事件(收起+拉伸) override func bindActions() { super.bindActions() } //侧边栏收起方法 @objc func hideSidebar() { let sidebarIndex = 0 let expandWidth : CGFloat = 160 let collapsedWidth : CGFloat = 48 //当侧边栏收起时 if isSidebarCollapsed{ //收起---展开 NSAnimationContext.runAnimationGroup{ context in context.duration = 0.3 // Set the duration of the animation self.eventCenterSplitView.setPosition(expandWidth, ofDividerAt: sidebarIndex) } }else { //展开--收起 NSAnimationContext.runAnimationGroup{ context in context.duration = 0.3 // Set the duration of the animation self.eventCenterSplitView.setPosition(collapsedWidth, ofDividerAt: sidebarIndex) } } isSidebarCollapsed.toggle() } } // MARK: -侧边栏Controller class class TPGuardSidebarController:TPBaseViewController{ override func viewDidLoad() { super.viewDidLoad() // Do view setup here. view.wantsLayer = true view.layer?.backgroundColor = NSColor.tpBackground.cgColor; } } // MARK: -侧边栏Controller扩展 extension TPGuardEventCenterController: NSSplitViewDelegate { //subview 这个子视是否允许被折叠(即宽度/高度小到 0)?” func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool { return false } func splitView(_ splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { return 48 } func splitView(_ splitView: NSSplitView, constrainMaxCoordinate proposedMaximumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { return 160 } //“当用户拖动分隔条时,询问代理:是否允许系统自动调整 view 这个子视的大小?” func splitView(_ splitView: NSSplitView, shouldAdjustSizeOfSubview view: NSView) -> Bool { return false } // func splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool { // return deviceListHasHidden // } // func splitView(_ splitView: NSSplitView, effectiveRect proposedEffectiveRect: NSRect, forDrawnRect drawnRect: NSRect, ofDividerAt dividerIndex: Int) -> NSRect { // return deviceListHasHidden ? NSRect.zero : proposedEffectiveRect // } // func splitViewWillResizeSubviews(_ notification: Notification) { // if deviceListViewController.view.frame.size.width > 500 { // deviceListViewController.view.frame.size = NSSize(width: 500, height: deviceListViewController.view.frame.size.height) // } // } } // MARK: - 事件中心split class class TPGuardEventCenterSpliteView: NSSplitView { override var dividerColor: NSColor { return NSColor.tpSeparator } override var dividerThickness: CGFloat { return 1 } } // MARK: outlineview数据与界面 extension TPGuardEventCenterController: NSOutlineViewDelegate, NSOutlineViewDataSource, TPGuardOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { guard let sidebarItem = item as? SidebarItem else { return sidebarItems.count } return sidebarItem.children?.count ?? 0 } func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { guard let sidebarItem = item as? SidebarItem else { return false } return sidebarItem.children?.isEmpty == false } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { guard let sidebarItem = item as? SidebarItem else { return sidebarItems[index] } return sidebarItem.children![index] } func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { guard let sidebarItem = item as? SidebarItem else { return nil } //唯一标记一种类型的表格单元格(cell)实现 NSOutlineView 或 NSTableView 的 cell 复用机制 let identifier = NSUserInterfaceItemIdentifier("SidebarCell") //尝试从复用池中获取一个已经存在的、可重用的单元格 var sidebarCell = outlineView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView if sidebarCell == nil { sidebarCell = NSTableCellView() sidebarCell?.identifier = identifier // icon let sidebarImageView = NSImageView() sidebarImageView.translatesAutoresizingMaskIntoConstraints = false sidebarCell?.addSubview(sidebarImageView) sidebarImageView.tag = 100 sidebarImageView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(0) make.centerY.equalToSuperview() make.width.height.equalTo(18) } // title let sidebarTextLabel = NSTextField(labelWithString: "") sidebarTextLabel.translatesAutoresizingMaskIntoConstraints = false sidebarTextLabel.textColor = .white // sidebarTextLabel.font = NSFont.systemFont(ofSize: 12) sidebarCell?.addSubview(sidebarTextLabel) sidebarTextLabel.tag = 101 sidebarTextLabel.snp.makeConstraints { make in make.leading.equalTo(sidebarImageView.snp.trailing).offset(8) make.centerY.equalToSuperview() } // unread count // let sidebarBadgeWidth: CGFloat = 20; // let sidebarBadgeHeigth: CGFloat = 14; // let sidebarBadgeLabel = TPBLabel(); // sidebarBadgeLabel.textColor = .white; // sidebarBadgeLabel.wantsLayer = true; // sidebarBadgeLabel.layer?.backgroundColor = NSColor.red.cgColor; // sidebarBadgeLabel.textAlignment = .center; // sidebarBadgeLabel.layer?.cornerRadius = (sidebarBadgeHeigth - 2) / 2; // sidebarBadgeLabel.layer?.masksToBounds = true; // sidebarBadgeLabel.layer?.borderWidth = 1.0; // sidebarBadgeLabel.layer?.borderColor = NSColor.red.cgColor; // unread count let sidebarBadgeLabel = NSTextField(labelWithString: "") sidebarBadgeLabel.translatesAutoresizingMaskIntoConstraints = false sidebarBadgeLabel.wantsLayer = true sidebarBadgeLabel.layer?.backgroundColor = NSColor.systemRed.cgColor sidebarBadgeLabel.layer?.cornerRadius = 6 sidebarBadgeLabel.textColor = .white sidebarBadgeLabel.alignment = .center sidebarBadgeLabel.font = NSFont.systemFont(ofSize: 10) sidebarCell?.addSubview(sidebarBadgeLabel) sidebarBadgeLabel.tag = 102 sidebarBadgeLabel.snp.makeConstraints { make in make.leading.equalTo(sidebarTextLabel.snp.trailing).offset(8) make.centerY.equalTo(sidebarTextLabel.snp.centerY)//对齐文字 make.width.greaterThanOrEqualTo(16) make.height.equalTo(16) } } if let sidebarImageView = sidebarCell?.viewWithTag(100) as? NSImageView { sidebarImageView.image = sidebarItem.iconImage } if let sidebarTextLabel = sidebarCell?.viewWithTag(101) as? NSTextField { sidebarTextLabel.stringValue = sidebarItem.title } if let sidebarBadgeLabel = sidebarCell?.viewWithTag(102) as? NSTextField { if sidebarItem.showsBadge, let unreadCount = sidebarItem.unreadCount, unreadCount > 0 { sidebarBadgeLabel.isHidden = false if unreadCount >= 99 { sidebarBadgeLabel.stringValue = "\(unreadCount)+" }else{ sidebarBadgeLabel.stringValue = "\(unreadCount)" } } else { sidebarBadgeLabel.isHidden = true } } return sidebarCell } // 进 func outlineView(_ outlineView: NSOutlineView, indentationForItem item: Any?) -> CGFloat { return 0 } }
09-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值