#macos #cocoa #core-graphics #quartz-2d #setneedsdisplay
#macos #cocoa #ядро-графика #quartz-2d #setneedsdisplay
Вопрос:
Я разрабатываю приложение для визуализации звука для macOS, и я хочу использовать Quartz / CoreGraphics для визуализации изменяющегося во времени спектра, согласованного с воспроизводимым звуком. Мой код рендеринга:
import Cocoa
средство визуализации класса: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.white.setFill()
bounds.fill()
guard let context = NSGraphicsContext.current?.cgContext else {return}
var x : CGFloat = 0.0
var y : CGFloat = 0.0
context.beginPath()
context.move(to: CGPoint(x: x, y: y))
for bin in 0 ..< 300 {
x = CGFloat(bin)
y = CGFloat(Global.spectrum[bin])
context.addLine(to: CGPoint(x: x, y: y))
}
context.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
context.setLineWidth(1.0)
context.strokePath()
self.setNeedsDisplay(dirtyRect)
}
}
При этом путь рисуется один раз — с использованием начальных нулевых значений массива spectrum[] — и затем продолжает рисовать ту же самую нулевую линию бесконечно. Он не обновляется с использованием новых значений в массиве spectrum[]. Я использовал оператор print(), чтобы проверить, что сами значения обновляются, но функция draw не перерисовывает путь, используя обновленные значения spectrum. Что я делаю не так?
Комментарии:
1. Когда вы обновляете значения, вы запрашиваете обновление представления, используя что-то вроде setNeedsDisplay ? draw будет вызываться только в том случае, если система считает, что область окна необходимо перерисовать. Это не вызывается при каждом обновлении.
2. Последняя строка в приведенном выше коде — «setNeedsDisplay». Но, похоже, это не приносит никакой пользы. Я подозреваю, что я неправильно его использую. Как мне вызывать перерисовку при каждом обновлении или, по крайней мере, запрашивать ее после каждого рисования пути?
3. Вы будете рисовать в виде с фиксированной шириной или в виде прокрутки? Как организованы данные, т.Е. Получаете ли вы пакет точек через определенные промежутки времени, и если да, то где код для этого? Вы хотите периодически обновлять весь вид или добавлять точки данных с одной или другой стороны, т.Е. Слева направо или справа налево?
4. Прокрутка не требуется. Я просто преобразую изменяющийся во времени спектр в NSRect фиксированного размера. spectrum [bin] — это одномерный массив с плавающими значениями, предоставляемый AVAudioEngine, и БПФ, обновляемый каждые 100 миллисекунд. (Код для этого длинный и не имеет отношения к моей проблеме рендеринга.) Я хочу перерисовать весь путь (не добавлять к нему точки) из массива spectrum, который обновляется со скоростью 10 кадров в секунду.
5. Место в моем приложении, где обновляется массив spectrum, — это мой класс AudioManager, который расширяет NSObject. Он ничего не знает о NSView, или NSRect, или dirtyRect, которые понадобились бы для вставки «setNeedsDisplay». Итак, я согласен с вами, что вызов «setNeedsDisplay» является ключевым для решения моей проблемы, но ни один из способов, которыми я пытался его вызвать, не сработал.
Ответ №1:
В следующей демонстрации показано, как обновить NSView с помощью случайных чисел, созданных таймером в отдельном классе, чтобы, надеюсь, имитировать ваш проект. Его можно запустить в Xcode, настроив проект Swift для macOS, скопировав / вставив исходный код в новый файл с именем main.swift и удалив AppDelegate, предоставленный Apple. Используется функция рисования, аналогичная той, которую вы опубликовали.
import Cocoa
var view : NSView!
var data = [Int]()
public extension Array where Element == Int {
static func generateRandom(size: Int) -> [Int] {
guard size > 0 else {
return [Int]()
}
return Array(0..<size).shuffled()
}
}
class DataManager: NSObject {
var timer:Timer!
@objc func fireTimer() {
data = Array.generateRandom(size:500)
view.needsDisplay = true
}
func startTimer(){
timer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}
func stopTimer() {
timer?.invalidate()
}
}
let dataMgr = DataManager()
class View: NSView {
override func draw(_ rect: NSRect) {
super.draw(rect)
NSColor.white.setFill()
bounds.fill()
guard let gc = NSGraphicsContext.current?.cgContext else {return}
var xOld : CGFloat = 0.0
var yOld : CGFloat = 0.0
var xNew : CGFloat = 0.0
var yNew : CGFloat = 0.0
var counter : Int = 0
gc.beginPath()
gc.move(to: CGPoint(x: xOld, y: yOld))
for i in 0 ..< data.count {
xNew = CGFloat(counter)
yNew = CGFloat(data[i])
gc.addLine(to: CGPoint(x: xNew, y: yNew))
xOld = xNew;
yOld = yNew;
counter = counter 1
}
gc.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
gc.setLineWidth(1.0)
gc.strokePath()
}
}
class ApplicationDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
@objc func myStartAction(_ sender:AnyObject ) {
dataMgr.startTimer()
}
@objc func myStopAction(_ sender:AnyObject ) {
dataMgr.stopTimer()
}
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() {
data = Array.generateRandom(size: 500)
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)
// **** Start Button **** //
let startBtn = NSButton (frame:NSMakeRect( 30, 20, 95, 30 ))
startBtn.bezelStyle = .rounded
startBtn.title = "Start"
startBtn.action = #selector(self.myStartAction(_:))
window.contentView!.addSubview (startBtn)
// **** Stop Button **** //
let stopBtn = NSButton (frame:NSMakeRect( 230, 20, 95, 30 ))
stopBtn.bezelStyle = .rounded
stopBtn.title = "Stop"
stopBtn.action = #selector(self.myStopAction(_:))
window.contentView!.addSubview (stopBtn)
// **** Custom view **** //
view = View( frame:NSMakeRect(20, 60, _wndW - 40, _wndH - 80))
view.autoresizingMask = [.width, .height]
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 applicationDelegate = ApplicationDelegate()
// **** main.swift **** //
let application = NSApplication.shared
application.setActivationPolicy(NSApplication.ActivationPolicy.regular)
application.delegate = applicationDelegate
application.activate(ignoringOtherApps:true)
application.run()
Комментарии:
1. apodidae, большое вам спасибо за рекомендованное решение. Я скопировал ваш код и создал тестовый проект, как вы предложили. Он запускается и показывает график, изменяющийся во времени. Теперь мне нужно изучить ваш код, чтобы понять, как я могу адаптировать его к своему проекту. Предварительно, я думаю, что вы решили мою проблему. Еще раз спасибо.
2. apodidae, спасибо за решение моей проблемы. Позвольте мне обобщить ваше решение: (1) Поместите «var view: NSView» на верхний уровень в моем проекте вне структуры классов. Это объявляет view как глобальную переменную. (2) Вставьте «‘view.needsDisplay = true» сразу после инструкции в моем проекте, которая обновляет данные (в моем случае, после того, как AudioManager обновит массив spectrum[]). (3) Убедитесь, что view фактически указывает на тот же NSView, в который обращается мой класс визуализации — используя «view = Renderer( frame: NSMakeRect ( ) )» и «window.contentView!.addSubview (view)» при настройке структуры моего проекта.