【问题标题】:Best methods for drawing an interactive 2D plot in MacOS在 MacOS 中绘制交互式 2D 绘图的最佳方法
【发布时间】:2020-09-19 03:35:21
【问题描述】:

我正在用 Swift 开发一个 MacOS 应用程序,用户可以在其中根据 2D 时间线记录关键帧值。我的点存储为字典,其中键是时间,值是当时对应的值。

我完全没有问题将我的点绘制为静态 2D 线图,并在线上画圈以指示实际记录的数据点。即使有非常多的数据点,绘制线条也非常容易,甚至包括在记录期间“实时”重新计算和绘制线条。

然后我卡住的地方是将我的 2D 绘图转换为“交互式”绘图,用户可以在其中选择一个或多个点,然后拖动它们,更改相应数据中存储的点。

到目前为止,我一直在尝试在我的 NSView 中绘制我的线,然后我没有为每个点绘制圆作为 NSBezierPath,而是为每个绘制圆的点创建一个自定义 NSView 的实例,并包含所有mouseDown/mouseDragged/等。功能并将其添加为主线视图的子视图。虽然这种“技术上”有效,但它会在计算并丢弃所有这些视图时使机器停止运行,显然更多的点等于更多的阻力。

我是否错过了以更有效的方式完成此任务的某种方式?我一直在搜索,虽然有一百万零个框架和方法用于绘制大型数据集的 2D 图,但我还没有找到一个具有“交互式”用户操作数据的示例。

【问题讨论】:

  • 您将使用多少个数据点? NSPoint 数组是不可能的吗?
  • 数百,可能数千。我想我可以很容易地将我的点字典转换为 NSPoint 数组。使用 NSPoint 有一些显示优势吗?
  • 我一直在这样的项目中使用 NSPoint 数组,并且没有使用字典的经验。我将采取的方法是这样的:1)获取数据并将其存储在一个数组中 2)使用正方形或圆形为数据点创建一条数据路径并用线连接。 3) 使用 mouseDown 事件检查 ptInRect 4) 允许使用鼠标重新定位数据 pt,相应地更新数组并重绘路径。
  • 嗯...所以在 mouseDown 上,您只需对数组中的所有点进行 ptInRect 检查吗?我可以尝试一下,因为它可以解决我的问题。我还需要能够一次选择拖动多个点,所以我将不得不尝试如何跟踪哪些点被选中或未选中,但我认为这是一个很有前途的想法。谢谢!
  • >so on a mouseDown 你只是做一个 ptInRect 检查迭代你数组中的所有点?正确的。我没有多点选择的经验。

标签: swift macos core-graphics


【解决方案1】:

此演示使用 NSPoints 的数据数组来创建一个交互式图形,该图形允许使用鼠标单独移动数据点。它不解决多点选择问题,需要按比例放大以满足您的需求(我没有用超过 20 个点对其进行测试;希望它不会陷入困境)。该演示可以从终端或 Xcode 中的命令行运行,方法是添加一个“main.swift”文件并将现有的 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()

【讨论】:

  • 我并没有完全走这条路,但即使使用我当前的数据结构,我似乎缺少的关键是按照您的建议处理在单个平面上绘制点和线,而不是尝试为每个点添加一个唯一的子视图,然后跟踪每个点的 NSRect 以进行 mouseDown 命中测试。所以我很高兴将此标记为已回答。谢谢!
猜你喜欢
  • 1970-01-01
  • 2016-05-08
  • 1970-01-01
  • 2010-11-26
  • 1970-01-01
  • 2011-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多