Başlangıçta C#’ ın sürükle bırak mantığı ile arayüz hazırlayan birisi olarak Python arayüz konusunda beni çok zorladı. Tkinter denemelerimde bu işi çok uğraştırıcı olduğunu görüp vazgeçmiştim. QT ile tanıştığımda drag and drop tasarımın rahatlığını tekrar yakalayıp Python’a daha çok heveslendiğimi söyleyebilirim. Asıl ihtiyacım olan mouse ile sürüklenebilir noktalı grafik arayüzü için çeşitli örnek arayışlarına girdim fakat hazır çözümler beni çok uğraştırdı. Bu yüzden örnek aramayı bırakıp sıfırdan kendim bir yapı hazırladım. Bu yazıda Matplotlib ile “Drag-Drop Point” grafik yapmaya başlayalım.
Tasarıma bir adet widget eklemek ilk aşama için yeterli.
Daha sonra “myGui.ui” olarak kaydedilen QT Designer çalışmasını python koduna gönüştürmek için convert.bat isimli şu dosyayı oluşturup çalıştırabilirsiniz.
1 |
pyuic5 myGui.ui -o gui.py |
Dosyayı çalıştırdığınızda size gui.py dosyasını üretecek.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'myGui.ui' # # Created by: PyQt5 UI code generator 5.15.2 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(860, 590) self.widget1 = QtWidgets.QWidget(Dialog) self.widget1.setGeometry(QtCore.QRect(10, 10, 840, 571)) self.widget1.setObjectName("widget1") self.retranslateUi(Dialog) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Drag-Drop Graph")) |
Bu aşamadan sonra tek yapmanız gereken ana çalışma dosyanıza bu python dosyasını dahil etmek. Matplotlib ile arayüzdeki widgeti bağlamak için aşağıdaki örnek incelenebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import sys from PyQt5.QtWidgets import QDialog, QApplication, QVBoxLayout from matplotlib.figure import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from gui import Ui_Dialog class myDialog(QDialog, Ui_Dialog): def __init__(self): super(myDialog, self).__init__() self.setupUi(self) self.fig1 = Figure() # type:Figure self.ax1 = self.fig1.add_subplot(1, 1, 1) self.ax1.grid(True) self.ax1.set_xlim((-1, 9)) self.ax1.set_ylim((-1, 9)) self.ax1.set_xlabel('x') self.ax1.set_ylabel('y') canvas = FigureCanvasQTAgg(self.fig1) # toolbar = NavigationToolbar(canvas, self) layout1 = QVBoxLayout() layout1.addWidget(canvas) # layout1.addWidget(toolbar) self.widget1.setLayout(layout1) self.widget1.show() if __name__ == "__main__": app = QApplication(sys.argv) myWindow = myDialog() myWindow.show() sys.exit(app.exec_()) |
Yukarıdaki örnekte önce bir figure tanımlaması yapılıp bir subplot oluşturduktan sonra bu figure canvas’a atanır ve bir layout tanımlanarak bu görsel layout a yüklenir. Hazırlanan layout widget a aktarılarak show metodu çalıştırıl. Daha sonra tüm grafik işlemlerini figure ve ax üzerinden yaparız.
Sürüklenebilir noktaları yapmak adına öncelikle nokta sınıfı oluşturarak işe başlayalım. Bu noktalar sürükleneceği için sınıf içerisinde koordinat erişimi, noktayı tanımlayacak bir kimlik (id) ve noktayı güncelleyecek methodlara ihtiyacımız var.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class PointCircle(object): def __init__(self, id, x, y, radius): self.id = id self.x = x self.y = y self.radius = radius self.mypatch = mpl.patches.Circle((self.x, self.y), self.radius, color='black', clip_on=False, picker=10) # self.point_circle = plt.Circle((self.x, self.y), self.radius, color='r', clip_on=False, picker=10) def get_xy(self): return self.x, self.y def get_radius(self): return self.radius def get_points(self): return self.mypatch def get_id(self): return self.id def update(self, x, y): self.x = x self.y = y self.mypatch = mpl.patches.Circle((self.x, self.y), self.radius, color='black', clip_on=False, picker=10) |
Örnekte 2 adet nokta oluşturup aralarına doğrusal bir çizgi çekelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
self.selected_circle = None self.point_circle_count = 2 self.point_circle_list = [] self.point_circle_list.append(PointCircle(0, 0, 0, 0.2)) self.point_circle_list.append(PointCircle(1, 2, 3, 0.2)) self.ax1.add_patch(self.point_circle_list[0].get_points()) self.ax1.add_patch(self.point_circle_list[1].get_points()) self.pick_handler = self.on_pick(self.point_circle_list) canvas = FigureCanvasQTAgg(self.fig1) canvas.mpl_connect('motion_notify_event', self.mouse_move) canvas.mpl_connect('button_release_event', self.mouse_release) canvas.mpl_connect('pick_event', self.pick_handler) |
Mouse ile noktaların alanlarını seçebilmek için pick event özelliğini kendi tanımladığımız bir metoda bağladık. Bu sayede noktaya tıkladığımızda bir olay oluşmasını sağlayabiliriz.
Peki hangi noktaya tıkladığımızı nasıl anlarız ? Sınıf tanımında yapılan id tanımı ile bu noktalara bir kimlik verip pick event içerisinde bunu anlamak mümkün
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
def on_pick(self, circle_list): def pick_event(event): for i in range(0, self.point_circle_count): if event.artist == circle_list[i].get_points(): self.selected_circle = i # print(i, '. selected') return return pick_event def mouse_move(self, event): if not event.inaxes: return x, y = event.xdata, event.ydata if self.selected_circle == None: return self.point_circle_list[self.selected_circle].update(x, y) self.plot() # print(x, y) def mouse_release(self, event): if not event.inaxes: return self.selected_circle = None x, y = event.xdata, event.ydata |
Yukarıda mouse_release ile seçilen noktayı None tanımlayarak olası hatanın önüne geçmeyi unutmayın. Çünkü biz mouse hareket ettikçe güncellenen bir grafik istiyoruz. Tıklamayı bıraktığımızda hatalı güncellemelerin olmaması gerekiyor.
Son aşamada ise plot fonksiyonu kalıyor. Bu metot aslında oldukça basit. Bunun sebebi mouse move event içerisinde zaten tıklanan noktanın x ve y değerlerini güncelliyor olmamız. Geriye sadece ekranı temizleyip güncellemek kalıyor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
def plot(self): self.ax1.clear() self.ax1.grid(linestyle='-', linewidth=1) self.ax1.set_xlim((-1, 9)) self.ax1.set_ylim((-1, 9)) self.ax1.set_xlabel('x') self.ax1.set_ylabel('y') self.ax1.add_patch(self.point_circle_list[0].get_points()) self.ax1.add_patch(self.point_circle_list[1].get_points()) self.ax1.plot((self.point_circle_list[0].get_xy()[0], self.point_circle_list[1].get_xy()[0]), (self.point_circle_list[0].get_xy()[1], self.point_circle_list[1].get_xy()[1]), color='red') self.ax1.annotate('Text 1', # this is the text self.point_circle_list[0].get_xy(), # this is the point to label textcoords="offset points", # how to position the text xytext=(10, 10), # distance from text to points (x,y) ha='center', bbox=dict(boxstyle="round", fc="0.8")) self.ax1.annotate('Text 2', self.point_circle_list[1].get_xy(), # this is the point to label textcoords="offset points", # how to position the text xytext=(10, 10), # distance from text to points (x,y) ha='center', bbox=dict(boxstyle="round", fc="0.8")) # dotted horizontal line self.ax1.axhline(y=self.point_circle_list[1].get_xy()[1], color="black", linestyle=":") self.fig1.canvas.draw() |
Burada ax temizlendikten sonra görsel güzellik açısından bir kaç ekstra kod bulunmaktadır. Örneğin limitlerin tekrar güncellenmesi ve noktalara yazı eklenmesi hatta iki nokta arası çizgi çizmek de buna dahil. Bu işlemlerden sonra canvas draw ile güncelleme yaptığınızda çalışma bu noktada sonlanıyor.
Özetle python kullanmak programcıya çok büyük bir zaman tasarrufu sağlıyor diyebilirim. Gözümüzde büyüyen grafik işleri bile bir kaç saatlik çalışma ile hızlıca sonuç bulabiliyor. Proje dosyasının tam hali aşağıda bulunmaktadır.
Sürükleme işleminden sonrası aşağıdaki gibi olmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
import sys from PyQt5.QtWidgets import QDialog, QApplication, QVBoxLayout from matplotlib.figure import Figure import matplotlib as mpl from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from gui import Ui_Dialog class PointCircle(object): def __init__(self, id, x, y, radius): self.id = id self.x = x self.y = y self.radius = radius self.mypatch = mpl.patches.Circle((self.x, self.y), self.radius, color='black', clip_on=False, picker=10) # self.point_circle = plt.Circle((self.x, self.y), self.radius, color='r', clip_on=False, picker=10) def get_xy(self): return self.x, self.y def get_radius(self): return self.radius def get_points(self): return self.mypatch def get_id(self): return self.id def update(self, x, y): self.x = x self.y = y self.mypatch = mpl.patches.Circle((self.x, self.y), self.radius, color='black', clip_on=False, picker=10) class myDialog(QDialog, Ui_Dialog): def __init__(self): super(myDialog, self).__init__() self.setupUi(self) self.fig1 = Figure() # type:Figure self.ax1 = self.fig1.add_subplot(1, 1, 1) self.ax1.grid(True) self.ax1.set_xlim((-1, 9)) self.ax1.set_ylim((-1, 9)) self.ax1.set_xlabel('x') self.ax1.set_ylabel('y') self.selected_circle = None self.point_circle_count = 2 self.point_circle_list = [] self.point_circle_list.append(PointCircle(0, 0, 0, 0.2)) self.point_circle_list.append(PointCircle(1, 2, 3, 0.2)) self.ax1.add_patch(self.point_circle_list[0].get_points()) self.ax1.add_patch(self.point_circle_list[1].get_points()) self.pick_handler = self.on_pick(self.point_circle_list) canvas = FigureCanvasQTAgg(self.fig1) canvas.mpl_connect('motion_notify_event', self.mouse_move) canvas.mpl_connect('button_release_event', self.mouse_release) canvas.mpl_connect('pick_event', self.pick_handler) # toolbar = NavigationToolbar(canvas, self) layout1 = QVBoxLayout() layout1.addWidget(canvas) # layout1.addWidget(toolbar) self.widget1.setLayout(layout1) self.widget1.show() self.plot() def plot(self): self.ax1.clear() self.ax1.grid(linestyle='-', linewidth=1) self.ax1.set_xlim((-1, 9)) self.ax1.set_ylim((-1, 9)) self.ax1.set_xlabel('x') self.ax1.set_ylabel('y') self.ax1.add_patch(self.point_circle_list[0].get_points()) self.ax1.add_patch(self.point_circle_list[1].get_points()) self.ax1.plot((self.point_circle_list[0].get_xy()[0], self.point_circle_list[1].get_xy()[0]), (self.point_circle_list[0].get_xy()[1], self.point_circle_list[1].get_xy()[1]), color='red') self.ax1.annotate('Text 1', # this is the text self.point_circle_list[0].get_xy(), # this is the point to label textcoords="offset points", # how to position the text xytext=(10, 10), # distance from text to points (x,y) ha='center', bbox=dict(boxstyle="round", fc="0.8")) self.ax1.annotate('Text 2', self.point_circle_list[1].get_xy(), # this is the point to label textcoords="offset points", # how to position the text xytext=(10, 10), # distance from text to points (x,y) ha='center', bbox=dict(boxstyle="round", fc="0.8")) # dotted horizontal line self.ax1.axhline(y=self.point_circle_list[1].get_xy()[1], color="black", linestyle=":") self.fig1.canvas.draw() def on_pick(self, circle_list): def pick_event(event): for i in range(0, self.point_circle_count): if event.artist == circle_list[i].get_points(): self.selected_circle = i # print(i, '. selected') return return pick_event def mouse_move(self, event): if not event.inaxes: return x, y = event.xdata, event.ydata if self.selected_circle == None: return self.point_circle_list[self.selected_circle].update(x, y) self.plot() # print(x, y) def mouse_release(self, event): if not event.inaxes: return self.selected_circle = None x, y = event.xdata, event.ydata # print(x, y) if __name__ == "__main__": app = QApplication(sys.argv) myWindow = myDialog() myWindow.show() sys.exit(app.exec_()) |
Yine bir python projesinde bana büyük destek olan sayın Muhittin KAPLAN’a bir teşekkürü borç bilirim.
Bir başka yazıda görüşmek üzere. Esen kalın.