在 MacOS 中绘制交互式二维绘图的最佳方法
Best methods for drawing an interactive 2D plot in MacOS
我正在 Swift 中开发一个 MacOS 应用程序,用户可以在其中根据 2D 时间轴记录关键帧值。我的积分存储为字典,其中键是时间,值是当时对应的值。
我完全没有问题将我的点绘制为静态二维线图,并在线上绘制圆圈以指示实际记录的数据点。即使有非常多的数据点,绘制线条也非常敏捷,甚至包括在记录期间重新计算和绘制 "real-time" 中的线条。
我卡住的地方是将我的 2D 图转换成 "interactive" 图,用户可以在其中 select 一个或多个点,然后四处拖动它们,改变存储的点相应的数据。
到目前为止,我一直在尝试在我的 NSView 中绘制我的线,然后我没有为每个点绘制圆作为 NSBezierPath,而是为每个绘制圆并包含的点创建一个自定义 NSView 的实例所有 mouseDown/mouseDragged/etc。功能并将其添加为主线视图的子视图。虽然这个 "technically" 有效,但它会在计算和丢弃所有这些视图时让机器停止运转,显然更多的点等于更多的阻力。
我是否错过了以更有效的方式完成此任务的方法?我一直在搜索,虽然有一百万零一个框架和方法用于绘制大型数据集的二维图,但我还没有找到一个具有 "interactive" 用户操作数据的示例。
此演示使用 NSPoints 数据数组创建交互式图形,允许使用鼠标单独移动数据点。它不涉及多点选择,需要按比例放大以满足您的需求(我没有测试超过 20 个点;希望它不会陷入困境)。该演示可以是 运行 从终端命令行或 Xcode 中添加一个 'main.swift' 文件并用下面相应的 class 替换现有的 AppDelegate。
// May be run in Terminal using:
// swiftc ptinrect.swift -framework Cocoa -o ptinrect && ./ptinrect
// or in Xcode with instructions above.
import Cocoa
let path = NSBezierPath()
var R = [NSRect]()
var selected = [Bool]()
var data = [NSPoint]()
class CustomView: NSView {
override func draw(_ rect: NSRect ) {
let bkgrnd = NSBezierPath(rect: rect)
NSColor.lightGray.set()
bkgrnd.fill()
// circles
NSColor.black.set()
for x in stride(from:0, to:R.count, by:1) {
let circle = NSBezierPath(ovalIn:R[x])
circle.fill()
}
// lines
NSColor.white.set()
path.lineWidth = 2.0
path.move(to:data[0])
for x in stride(from:0, to:data.count, by:1) {
path.line(to:data[x])
}
path.stroke()
}
override func mouseDown(with event: NSEvent) {
let wndPt: NSPoint = event.locationInWindow
let pt:NSPoint = self.convert(wndPt, from: nil)
print(pt)
print(R.count)
for x in stride(from:0, to:R.count, by:1) {
if NSPointInRect(pt,R[x]){
selected[x] = true
print("mouse down in rect: \(x)")
print([selected])
} else {selected[x] = false}
}
}
override func mouseUp(with event: NSEvent) {
let wndPt: NSPoint = event.locationInWindow
let pt:NSPoint = self.convert(wndPt, from: nil)
print(pt)
for x in stride(from:0, to:R.count, by:1) {
if NSPointInRect(pt,R[x]){
print("mouse up in rect: \(x)")
}
}
path.removeAllPoints()
self.needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
let wndPt: NSPoint = event.locationInWindow
let pt:NSPoint = self.convert(wndPt, from: nil)
for x in stride(from:0, to:R.count, by:1) {
if (selected[x] == true){
print("mouse dragged in rect: \(x)")
data[x] = pt
R[x].origin.x = pt.x - 5
R[x].origin.y = pt.y - 5
}
}
path.removeAllPoints()
self.needsDisplay = true
}
}
class ApplicationDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func buildPath() {
var count: CGFloat = 0
for x in stride(from:0, to:20, by:1) {
let offset = CGFloat(count * 20.0)
data.append(NSPoint.init())
data[x] = NSMakePoint( 20 + offset, 20 + offset)
R.append(NSRect.init())
// x,y coordinates -5 to center rect on data pt
R[x] = NSMakeRect(data[x].x - 5,data[x].y - 5,10,10)
selected.append(Bool.init())
selected[x] = false
count += 1
}
print([data])
path.move(to:data[0])
for x in stride(from:0, to:data.count, by:1) {
path.line(to:data[x])
}
}
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 = 800
let _wndH : CGFloat = 600
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)
// **** Custom view **** //
let view = CustomView( frame:NSMakeRect(20, 60, _wndW - 40, _wndH - 80))
view.autoresizingMask = [.maxXMargin,.minYMargin, .height, .width]
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()
buildPath()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = ApplicationDelegate()
// **** main.swift **** //
let application = NSApplication.shared
application.setActivationPolicy(NSApplication.ActivationPolicy.regular)
application.delegate = appDelegate
application.activate(ignoringOtherApps:true)
application.run()
我正在 Swift 中开发一个 MacOS 应用程序,用户可以在其中根据 2D 时间轴记录关键帧值。我的积分存储为字典,其中键是时间,值是当时对应的值。
我完全没有问题将我的点绘制为静态二维线图,并在线上绘制圆圈以指示实际记录的数据点。即使有非常多的数据点,绘制线条也非常敏捷,甚至包括在记录期间重新计算和绘制 "real-time" 中的线条。
我卡住的地方是将我的 2D 图转换成 "interactive" 图,用户可以在其中 select 一个或多个点,然后四处拖动它们,改变存储的点相应的数据。
到目前为止,我一直在尝试在我的 NSView 中绘制我的线,然后我没有为每个点绘制圆作为 NSBezierPath,而是为每个绘制圆并包含的点创建一个自定义 NSView 的实例所有 mouseDown/mouseDragged/etc。功能并将其添加为主线视图的子视图。虽然这个 "technically" 有效,但它会在计算和丢弃所有这些视图时让机器停止运转,显然更多的点等于更多的阻力。
我是否错过了以更有效的方式完成此任务的方法?我一直在搜索,虽然有一百万零一个框架和方法用于绘制大型数据集的二维图,但我还没有找到一个具有 "interactive" 用户操作数据的示例。
此演示使用 NSPoints 数据数组创建交互式图形,允许使用鼠标单独移动数据点。它不涉及多点选择,需要按比例放大以满足您的需求(我没有测试超过 20 个点;希望它不会陷入困境)。该演示可以是 运行 从终端命令行或 Xcode 中添加一个 'main.swift' 文件并用下面相应的 class 替换现有的 AppDelegate。
// May be run in Terminal using:
// swiftc ptinrect.swift -framework Cocoa -o ptinrect && ./ptinrect
// or in Xcode with instructions above.
import Cocoa
let path = NSBezierPath()
var R = [NSRect]()
var selected = [Bool]()
var data = [NSPoint]()
class CustomView: NSView {
override func draw(_ rect: NSRect ) {
let bkgrnd = NSBezierPath(rect: rect)
NSColor.lightGray.set()
bkgrnd.fill()
// circles
NSColor.black.set()
for x in stride(from:0, to:R.count, by:1) {
let circle = NSBezierPath(ovalIn:R[x])
circle.fill()
}
// lines
NSColor.white.set()
path.lineWidth = 2.0
path.move(to:data[0])
for x in stride(from:0, to:data.count, by:1) {
path.line(to:data[x])
}
path.stroke()
}
override func mouseDown(with event: NSEvent) {
let wndPt: NSPoint = event.locationInWindow
let pt:NSPoint = self.convert(wndPt, from: nil)
print(pt)
print(R.count)
for x in stride(from:0, to:R.count, by:1) {
if NSPointInRect(pt,R[x]){
selected[x] = true
print("mouse down in rect: \(x)")
print([selected])
} else {selected[x] = false}
}
}
override func mouseUp(with event: NSEvent) {
let wndPt: NSPoint = event.locationInWindow
let pt:NSPoint = self.convert(wndPt, from: nil)
print(pt)
for x in stride(from:0, to:R.count, by:1) {
if NSPointInRect(pt,R[x]){
print("mouse up in rect: \(x)")
}
}
path.removeAllPoints()
self.needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
let wndPt: NSPoint = event.locationInWindow
let pt:NSPoint = self.convert(wndPt, from: nil)
for x in stride(from:0, to:R.count, by:1) {
if (selected[x] == true){
print("mouse dragged in rect: \(x)")
data[x] = pt
R[x].origin.x = pt.x - 5
R[x].origin.y = pt.y - 5
}
}
path.removeAllPoints()
self.needsDisplay = true
}
}
class ApplicationDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func buildPath() {
var count: CGFloat = 0
for x in stride(from:0, to:20, by:1) {
let offset = CGFloat(count * 20.0)
data.append(NSPoint.init())
data[x] = NSMakePoint( 20 + offset, 20 + offset)
R.append(NSRect.init())
// x,y coordinates -5 to center rect on data pt
R[x] = NSMakeRect(data[x].x - 5,data[x].y - 5,10,10)
selected.append(Bool.init())
selected[x] = false
count += 1
}
print([data])
path.move(to:data[0])
for x in stride(from:0, to:data.count, by:1) {
path.line(to:data[x])
}
}
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 = 800
let _wndH : CGFloat = 600
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)
// **** Custom view **** //
let view = CustomView( frame:NSMakeRect(20, 60, _wndW - 40, _wndH - 80))
view.autoresizingMask = [.maxXMargin,.minYMargin, .height, .width]
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()
buildPath()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = ApplicationDelegate()
// **** main.swift **** //
let application = NSApplication.shared
application.setActivationPolicy(NSApplication.ActivationPolicy.regular)
application.delegate = appDelegate
application.activate(ignoringOtherApps:true)
application.run()