CALayer 垂直居中文本 XCode 11.6
CALayer vertically centered text XCode 11.6
我正在构建一个 PDF 文档,图像和文本正在作为 CALayers 写入视图。我需要在 CATextLayer 的边框内垂直居中我的文本。我正在使用我从 2016 年发现的 class,如下所示,它覆盖了 draw 函数。我想知道是否有任何新技巧可以使它起作用?
正如您在 运行 这段代码中看到的,单元格 2 的文本甚至没有显示,单元格 3 的文本也没有垂直居中。
非常感谢任何能帮助我的人。
//
// ViewController.swift
// CALayers Example
//
// Created by Thomas Carroll on 8/18/20.
// Copyright © 2020 Thomas Carroll. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
let myLayers = MyLayers()
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
self.view.layer?.addSublayer(myLayers.insertGrid())
self.view.layer?.addSublayer(myLayers.insertText())
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
//
// MyLayers.swift
// CALayers Example
//
// Created by Thomas Carroll on 8/18/20.
// Copyright © 2020 Thomas Carroll. All rights reserved.
//
import Cocoa
// Set up constant variables
let pageWidth:Float = 72*8.5
let pageHeight:Float = 72*11
// Set up coordinates
let leftX = Int(pageWidth/2-72*2.5)
let col1X = Int(leftX+72)
let col2X = Int(col1X+72)
let col3X = Int(col2X+72)
let col4X = Int(col3X+72)
let rightX = Int(col4X+72)
let bottomY = Int(pageHeight/2-72*2.5)
let row4Y = Int(bottomY+72)
let row3Y = Int(row4Y+72)
let row2Y = Int(row3Y+72)
let row1Y = Int(row2Y+72)
let topY = Int(row1Y+72)
// Set the extension to draw Bezier paths into a CAShapeLayer
extension NSBezierPath {
// Credit - Henrick - 9/18/2016
//
public var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
print("Error occured in NSBezierPath extension.")
}
}
return path
}
}
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.height)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = (font?.capHeight)!
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = (height - lines * fontSize) / 2 - lines * fontSize / 10
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
context.restoreGState()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.clear.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
let cellText = ["This is some cell 1 text", "Cell 2 text", "This is text cell 3"]
for i in (0...2) {
let textLayer = VerticallyAlignedTextLayer()
textLayer.string = cellText[i]
textLayer.fontSize = 14
// Set the frame to be 1 pixel smaller than the grid cell to provide 1px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+1, y: Int(rowPosY[i])+1), size: CGSize(width: 70, height: 70))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
return myCALayer
}
}
这是我在系统上得到的。
我所做的只是向 func draw() 添加一些打印语句,以便我可以看到发生了什么,然后在 YDiff 计算前添加一个负号。在我看来,它没有显示文本,因为 Y 坐标导致它在您的框外的某个地方被翻译。此外,.translateBy 背后的评论说使用负 YDiff。
override func draw(in context: CGContext) {
let height = self.bounds.size.height
print("height = \(height)")
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
print("lines = \(lines)")
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 10
print("yDiff = \(yDiff)")
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
print("draw called.")
context.restoreGState()
}
以下代码是我使用的代码,但您似乎可以正常使用它。唯一的变化是 func draw() 如上所述。您可以从终端 运行 此代码或通过使用演示的主要部分并用演示的代码替换 Xcode 的 AppDelegate 并添加其他 类 来创建您自己的 Xcode 应用程序.
/*
To run in Terminal: swiftc calayer.swift -framework Cocoa -o calayer && ./calayer
*/
import Cocoa
// Set up constant variables
let pageWidth:Float = 72*8.5
let pageHeight:Float = 72*11
// Set up coordinates
let leftX = Int(pageWidth/2-72*2.5)
let col1X = Int(leftX+72)
let col2X = Int(col1X+72)
let col3X = Int(col2X+72)
let col4X = Int(col3X+72)
let rightX = Int(col4X+72)
let bottomY = Int(pageHeight/2-72*2.5)
let row4Y = Int(bottomY+72)
let row3Y = Int(row4Y+72)
let row2Y = Int(row3Y+72)
let row1Y = Int(row2Y+72)
let topY = Int(row1Y+72)
// Set the extension to draw Bezier paths into a CAShapeLayer
extension NSBezierPath {
// Credit - Henrick - 9/18/2016
//
public var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
print("Error occured in NSBezierPath extension.")
}
}
return path
}
}
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.height)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = (font?.capHeight)!
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
print("height = \(height)")
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
print("lines = \(lines)")
//let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 10
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 5
print("yDiff = \(yDiff)")
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
print("draw called.")
print("==========================")
context.restoreGState()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.clear.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
// let cellText = ["This is some cell 1 text", "Some cell 2 text", "This is text cell 3"]
let cellText = ["This is some cell 1 text", "Cell 2 text", "This is text cell 3", "hello"]
for i in (0...3) {
let textLayer = VerticallyAlignedTextLayer()
textLayer.string = cellText[i]
print(cellText[i])
textLayer.fontSize = 14
// Set the frame to be 1 pixel smaller than the grid cell to provide 1px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+1, y: Int(rowPosY[i])+1), size: CGSize(width: 70, height: 70))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
print("=======================")
return myCALayer
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
@objc func myBtnAction(_ sender:AnyObject ) {
NSSound.beep()
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 650
let _wndH : CGFloat = 700
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
let view = NSView()
let myLayers = MyLayers()
view.frame = NSMakeRect(20, 60, _wndW - 40, _wndH - 80)
view.wantsLayer = true
view.layer?.addSublayer(myLayers.insertGrid())
view.layer?.addSublayer(myLayers.insertText())
window.contentView!.addSubview (view)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()
Swift 5.3
好的,我终于想出了如何做我需要的事情。具体来说,我需要更改 textLayer 的 fontSize,使其水平和垂直适合边界框,同时在边界框内垂直居中。为此,我找到了一些检查属性字符串的 boundingRect 的代码。另请注意,我更改了 yDiff 计算,其中 fontSize 除以 6.5 而不是 10,它对我来说更好地垂直放置文本。
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.width)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = floor(font!.capHeight)
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(floor(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 // Use -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 when in non-flipped coordinates (like macOS's default)
context.saveGState()
context.translateBy(x: 0, y: yDiff)
super.draw(in: context)
context.restoreGState()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.white.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
//gridPath.close()
//gridLayer.path = gridPath.cgPath
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func sizeOfRect(string: NSString, fontSize: CGFloat) -> Int {
/* Credit to Jake Marsh - 12/10/2015
https://littlebitesofcocoa.com/144-drawing-multiline-strings
Return the height of a boundingRect for a specified string at a specified fontSize
*/
let cellFontSize:CGFloat = fontSize
let cellFont:NSFont = NSFont.systemFont(ofSize: cellFontSize, weight: .regular)
let cellParagraphStyle = NSMutableParagraphStyle()
let cellTextAttributes = [NSAttributedString.Key.font: cellFont, NSAttributedString.Key.paragraphStyle: cellParagraphStyle]
let cellDrawingOptions: NSString.DrawingOptions = [
.usesLineFragmentOrigin, .usesFontLeading]
cellParagraphStyle.lineHeightMultiple = 1.0
cellParagraphStyle.lineBreakMode = .byWordWrapping
return Int(string.boundingRect(with: CGSize(width: 70, height: CGFloat.infinity), options: cellDrawingOptions, attributes: cellTextAttributes).height)
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
let cellText = ["This is some cell 1 text that is kind of long", "Cell 2 text", "This is text cell 3", "Some really really long text"]
for i in (0...3) {
// Create a vertically centered textLayer
let textLayer = VerticallyAlignedTextLayer()
// Set up the initial font size for the text
var fontSize:CGFloat = 14
// Assign a string to the textLayer
textLayer.string = cellText[i]
// Check the vertical hieght of a rectangle that would contain the text based on the current fontSize. If the text is taler than the specific box height, reduce the fontSize my a half point until it is within the specified height of the box.
while sizeOfRect(string: cellText[i] as NSString, fontSize: fontSize) > 68 {
fontSize -= 0.5
}
// Assign the adjusted fontSize to the textLayer
textLayer.fontSize = fontSize
// Set the frame to be 4 pixel smaller than the grid cell to provide 2px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+2, y: Int(rowPosY[i])+2), size: CGSize(width: 68, height: 68))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
return myCALayer
}
}
我正在构建一个 PDF 文档,图像和文本正在作为 CALayers 写入视图。我需要在 CATextLayer 的边框内垂直居中我的文本。我正在使用我从 2016 年发现的 class,如下所示,它覆盖了 draw 函数。我想知道是否有任何新技巧可以使它起作用?
正如您在 运行 这段代码中看到的,单元格 2 的文本甚至没有显示,单元格 3 的文本也没有垂直居中。
非常感谢任何能帮助我的人。
//
// ViewController.swift
// CALayers Example
//
// Created by Thomas Carroll on 8/18/20.
// Copyright © 2020 Thomas Carroll. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
let myLayers = MyLayers()
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
self.view.layer?.addSublayer(myLayers.insertGrid())
self.view.layer?.addSublayer(myLayers.insertText())
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
//
// MyLayers.swift
// CALayers Example
//
// Created by Thomas Carroll on 8/18/20.
// Copyright © 2020 Thomas Carroll. All rights reserved.
//
import Cocoa
// Set up constant variables
let pageWidth:Float = 72*8.5
let pageHeight:Float = 72*11
// Set up coordinates
let leftX = Int(pageWidth/2-72*2.5)
let col1X = Int(leftX+72)
let col2X = Int(col1X+72)
let col3X = Int(col2X+72)
let col4X = Int(col3X+72)
let rightX = Int(col4X+72)
let bottomY = Int(pageHeight/2-72*2.5)
let row4Y = Int(bottomY+72)
let row3Y = Int(row4Y+72)
let row2Y = Int(row3Y+72)
let row1Y = Int(row2Y+72)
let topY = Int(row1Y+72)
// Set the extension to draw Bezier paths into a CAShapeLayer
extension NSBezierPath {
// Credit - Henrick - 9/18/2016
//
public var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
print("Error occured in NSBezierPath extension.")
}
}
return path
}
}
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.height)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = (font?.capHeight)!
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = (height - lines * fontSize) / 2 - lines * fontSize / 10
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
context.restoreGState()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.clear.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
let cellText = ["This is some cell 1 text", "Cell 2 text", "This is text cell 3"]
for i in (0...2) {
let textLayer = VerticallyAlignedTextLayer()
textLayer.string = cellText[i]
textLayer.fontSize = 14
// Set the frame to be 1 pixel smaller than the grid cell to provide 1px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+1, y: Int(rowPosY[i])+1), size: CGSize(width: 70, height: 70))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
return myCALayer
}
}
这是我在系统上得到的。
我所做的只是向 func draw() 添加一些打印语句,以便我可以看到发生了什么,然后在 YDiff 计算前添加一个负号。在我看来,它没有显示文本,因为 Y 坐标导致它在您的框外的某个地方被翻译。此外,.translateBy 背后的评论说使用负 YDiff。
override func draw(in context: CGContext) {
let height = self.bounds.size.height
print("height = \(height)")
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
print("lines = \(lines)")
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 10
print("yDiff = \(yDiff)")
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
print("draw called.")
context.restoreGState()
}
以下代码是我使用的代码,但您似乎可以正常使用它。唯一的变化是 func draw() 如上所述。您可以从终端 运行 此代码或通过使用演示的主要部分并用演示的代码替换 Xcode 的 AppDelegate 并添加其他 类 来创建您自己的 Xcode 应用程序.
/*
To run in Terminal: swiftc calayer.swift -framework Cocoa -o calayer && ./calayer
*/
import Cocoa
// Set up constant variables
let pageWidth:Float = 72*8.5
let pageHeight:Float = 72*11
// Set up coordinates
let leftX = Int(pageWidth/2-72*2.5)
let col1X = Int(leftX+72)
let col2X = Int(col1X+72)
let col3X = Int(col2X+72)
let col4X = Int(col3X+72)
let rightX = Int(col4X+72)
let bottomY = Int(pageHeight/2-72*2.5)
let row4Y = Int(bottomY+72)
let row3Y = Int(row4Y+72)
let row2Y = Int(row3Y+72)
let row1Y = Int(row2Y+72)
let topY = Int(row1Y+72)
// Set the extension to draw Bezier paths into a CAShapeLayer
extension NSBezierPath {
// Credit - Henrick - 9/18/2016
//
public var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
print("Error occured in NSBezierPath extension.")
}
}
return path
}
}
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.height)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = (font?.capHeight)!
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
print("height = \(height)")
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
print("lines = \(lines)")
//let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 10
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 5
print("yDiff = \(yDiff)")
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
print("draw called.")
print("==========================")
context.restoreGState()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.clear.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
// let cellText = ["This is some cell 1 text", "Some cell 2 text", "This is text cell 3"]
let cellText = ["This is some cell 1 text", "Cell 2 text", "This is text cell 3", "hello"]
for i in (0...3) {
let textLayer = VerticallyAlignedTextLayer()
textLayer.string = cellText[i]
print(cellText[i])
textLayer.fontSize = 14
// Set the frame to be 1 pixel smaller than the grid cell to provide 1px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+1, y: Int(rowPosY[i])+1), size: CGSize(width: 70, height: 70))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
print("=======================")
return myCALayer
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
@objc func myBtnAction(_ sender:AnyObject ) {
NSSound.beep()
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 650
let _wndH : CGFloat = 700
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
let view = NSView()
let myLayers = MyLayers()
view.frame = NSMakeRect(20, 60, _wndW - 40, _wndH - 80)
view.wantsLayer = true
view.layer?.addSublayer(myLayers.insertGrid())
view.layer?.addSublayer(myLayers.insertText())
window.contentView!.addSubview (view)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()
Swift 5.3
好的,我终于想出了如何做我需要的事情。具体来说,我需要更改 textLayer 的 fontSize,使其水平和垂直适合边界框,同时在边界框内垂直居中。为此,我找到了一些检查属性字符串的 boundingRect 的代码。另请注意,我更改了 yDiff 计算,其中 fontSize 除以 6.5 而不是 10,它对我来说更好地垂直放置文本。
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.width)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = floor(font!.capHeight)
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(floor(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 // Use -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 when in non-flipped coordinates (like macOS's default)
context.saveGState()
context.translateBy(x: 0, y: yDiff)
super.draw(in: context)
context.restoreGState()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.white.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
//gridPath.close()
//gridLayer.path = gridPath.cgPath
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func sizeOfRect(string: NSString, fontSize: CGFloat) -> Int {
/* Credit to Jake Marsh - 12/10/2015
https://littlebitesofcocoa.com/144-drawing-multiline-strings
Return the height of a boundingRect for a specified string at a specified fontSize
*/
let cellFontSize:CGFloat = fontSize
let cellFont:NSFont = NSFont.systemFont(ofSize: cellFontSize, weight: .regular)
let cellParagraphStyle = NSMutableParagraphStyle()
let cellTextAttributes = [NSAttributedString.Key.font: cellFont, NSAttributedString.Key.paragraphStyle: cellParagraphStyle]
let cellDrawingOptions: NSString.DrawingOptions = [
.usesLineFragmentOrigin, .usesFontLeading]
cellParagraphStyle.lineHeightMultiple = 1.0
cellParagraphStyle.lineBreakMode = .byWordWrapping
return Int(string.boundingRect(with: CGSize(width: 70, height: CGFloat.infinity), options: cellDrawingOptions, attributes: cellTextAttributes).height)
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
let cellText = ["This is some cell 1 text that is kind of long", "Cell 2 text", "This is text cell 3", "Some really really long text"]
for i in (0...3) {
// Create a vertically centered textLayer
let textLayer = VerticallyAlignedTextLayer()
// Set up the initial font size for the text
var fontSize:CGFloat = 14
// Assign a string to the textLayer
textLayer.string = cellText[i]
// Check the vertical hieght of a rectangle that would contain the text based on the current fontSize. If the text is taler than the specific box height, reduce the fontSize my a half point until it is within the specified height of the box.
while sizeOfRect(string: cellText[i] as NSString, fontSize: fontSize) > 68 {
fontSize -= 0.5
}
// Assign the adjusted fontSize to the textLayer
textLayer.fontSize = fontSize
// Set the frame to be 4 pixel smaller than the grid cell to provide 2px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+2, y: Int(rowPosY[i])+2), size: CGSize(width: 68, height: 68))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
return myCALayer
}
}