def add_context_menu_actions(self, parent: QTableView, edit_path: QLineEdit): parent.setContextMenuPolicy(Qt.ActionsContextMenu) select_action = QAction('Выбрать все', self) deselect_action = QAction('Снять выделение со всех', self) rename_action = QAction('Переименовать', self) select_action.triggered.connect(self.select_all) deselect_action.triggered.connect(self.deselect_all) rename_action.triggered.connect( lambda checked, x=parent, y=edit_path: self.rename_file(x, y)) parent.addAction(select_action) parent.addAction(deselect_action) parent.addAction(rename_action)
class Ui_WebSetting(QWidget): # 1. Signal obj json_update_signal = pyqtSignal() def __init__(self, parent=None): super().__init__() self.setWindowTitle("Web ID/PW 관리") # 우하단 위젯 # self.setGeometry(300,300,500,10) rect = QDesktopWidget().availableGeometry( ) # 작업표시줄 제외한 화면크기 반환 # .screenGeometry() : 화면해상도 class (0, 0, x, y) max_x = rect.width() max_y = rect.height() width, height = 300, 300 left = max_x - width top = max_y - height self.setGeometry(left, top - 500, width, height) result = ddld_json_to_df() self.ddld_dic = result[0] self.idpw_dic_lst_dic = result[1] self._df = result[2] self.initUI() # self.show() def initUI(self): self.df = self._df self.emty_site_set = AddTable.item_set - set( self.df['website'].to_list()) self.model = PandasModel(self.df) self.view = QTableView() self.view.setModel(self.model) self.view.setSelectionBehavior( QTableView.SelectRows) # multiple row 단위 선택 가능 # self.view.setSelectionMode(QTableView.SingleSelection) # one row 선택 제한 self.view.setSelectionMode(QAbstractItemView.SingleSelection) # # # widget 창에 꽉 차게 http://bitly.kr/l8mzD7J https://hoy.kr/dFFaj self.view.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) # self.view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.view.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) # ===== # size policy : https://hoy.kr/dFFaj self.view.setAlternatingRowColors(True) self.view.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) self.view.setSizePolicy( QSizePolicy.Minimum, QSizePolicy.Minimum) # Expanding | Minimum | Maximum # header color : https://stackoverflow.com/questions/19198634/pyqt-qtablewidget-horizontalheaderlabel-stylesheet stylesheet = """QHeaderView::section{ background-color: #CEECF5; color: blue; }""" self.view.horizontalHeader().setStyleSheet(stylesheet) # right click menu 추가 : https://freeprog.tistory.com/334 [취미로 하는 프로그래밍 !!!] self.view.setContextMenuPolicy(Qt.ActionsContextMenu) del_action = QAction("선택행 삭제", self.view) self.view.addAction(del_action) del_action.triggered.connect(self.del_row) # ===== # # enable sorting : https://codeday.me/ko/qa/20190422/381866.html # # 삭제시 df 와 tablevew 간의 row index 일치시키기 위해 죽여놓음 # self.view.setSortingEnabled(True) le = QLineEdit('우클릭: 삭제 / 더블 클릭: 수정') le.setReadOnly(True) # Qt Style Sheets Examples : https://hoy.kr/d0nYi le.setStyleSheet( """QLineEdit { border: 2px solid gray; border-radius: 10px; padding: 2 15px; background: #f0f0f0; color: red; font: bold; selection-background-color: darkgray;} """) self.layout = QVBoxLayout() self.layout.addWidget(le) self.btn1 = QPushButton('저장 후 닫기', self) self.btn2 = QPushButton('웹사이트 ID/PW 추가', self) self.btn1.setStyleSheet( """QPushButton { background-color: #A9F5F2; color: blue; font: bold; border: 1px solid gray; padding: 5 0px;}""") self.btn2.setStyleSheet( """QPushButton { background-color: #A9F5F2; color: blue; font: bold; border: 1px solid gray; padding: 5 0px;}""") self.layout.addWidget(self.btn2) self.add_table = AddTable(self) self.add_table.setHidden(True) # toggle set self.layout.addWidget(self.add_table) self.layout.addWidget(self.view) self.layout.addWidget(self.btn1) self.setLayout(self.layout) # ======== 5. Signal connect Slot (동일 파일 내) self.make_connection(self.add_table) # self.make_connection_view(self.model) --- Main __main__ 으로 # ======== self.btn1.clicked.connect(self.save_df_to_json) self.btn2.clicked.connect(self.add_row) @pyqtSlot() def del_row(self): row_idx = self.view.selectionModel().currentIndex().row() self.df = self.df.drop(self.df.index[row_idx], axis=0) self.df.reset_index() self.model = PandasModel(self.df) self.view.setModel(self.model) self.btn1.setStyleSheet( """QPushButton { background-color: #ffff00; color: blue; font: bold }""" ) self.emty_site_set = AddTable.item_set - set( self.df['website'].to_list()) @pyqtSlot() # 엑셀저장 http://bitly.kr/cyM01Yn def save_df_to_json(self): #============= 3. Dataframe to original dic_lst_dic : https://hoy.kr/dXq62 # 1. 값 있는 것 df_to_dict = self.df.to_dict('split') _data_lst_lst = df_to_dict['data'] # 2. 값 없는 것 empty_site_lst = list(self.emty_site_set) empty_site_lst_lst = [[website, "", ""] for website in empty_site_lst] # 3. 1+2 data_lst_lst = _data_lst_lst + empty_site_lst_lst web_dic_lst_dic = dict() for web, id, pw in data_lst_lst: inner_dic = dict() inner_dic['id'] = id inner_dic['pw'] = pw if inner_dic['id'] != "": if web in web_dic_lst_dic.keys(): web_dic_lst_dic[web].append(inner_dic) # nd2 id/pw 이후 continue web_dic_lst_dic[web] = [inner_dic] # first id/pw continue web_dic_lst_dic[web] = [] # id/pw 가 없는 경우 #============ 4. dict to json # fulljs = r'C:\Ataxtech\ATT\Ver1.0\json\web.json' self.ddld_dic['idpw'] = web_dic_lst_dic with open(fulljs, 'w', encoding='utf-8') as fn: json.dump(self.ddld_dic, fn, ensure_ascii=False, indent=4) self.btn1.setStyleSheet( """QPushButton { background-color: #A9F5F2; color: blue; font: bold; border: 1px solid gray; padding: 5 0px;}""") # 2. Signal(json_update_signal) emit self.json_update_signal.emit() @pyqtSlot() def add_row(self): sender_obj = self.sender() if sender_obj.text() == "웹사이트 ID/PW 추가": self.add_table.setHidden(False) # toggle sender_obj.setText("ID/PW 저장") # sender_obj.setStyleSheet( # """QPushButton { background-color: #ffff00; color: blue; font: bold }""") elif sender_obj.text() == "ID/PW 저장": # print(self.add_table.website,'===', self.add_table.id, '===', self.add_table.pw) self.add_table.setHidden(True) # toggle sender_obj.setText("웹사이트 ID/PW 추가") # df에 리스트 바로 붙이기 https://hoy.kr/v8Krj if self.add_table.id != "" and self.add_table.pw != "": # id/pw 모두 입력 add_df_row_lst = [ self.add_table.website, self.add_table.id, self.add_table.pw ] self.df.loc[len(self.df) + 1] = add_df_row_lst self.model = PandasModel(self.df) self.view.setModel(self.model) self.add_table.setItem(0, 1, QTableWidgetItem("")) self.add_table.setItem(0, 2, QTableWidgetItem("")) sender_obj.setStyleSheet( """QPushButton { background-color: #A9F5F2; color: blue; font: bold }""" ) self.btn1.setStyleSheet( """QPushButton { background-color: #ffff00; color: blue; font: bold }""" ) self.emty_site_set = AddTable.item_set - set( self.df['website'].to_list()) # print(self.emty_site_set) # 3. Signal connect Slot def make_connection(self, signal_emit_object): signal_emit_object.pw_changed_signal.connect(self.receive_add_idpw) # 3. Signal connect Slot def make_connection_view(self, signal_emit_object): signal_emit_object.view_edit_signal.connect(self.receive_edit_idpw) # 3. Signal(json_update_signal) connect Slot def make_connection_json_update(self, signal_emit_object): signal_emit_object.json_update_signal.connect(self.receive_json_update) # 4. receive Signal @pyqtSlot() def receive_add_idpw(self): self.btn2.setStyleSheet( """QPushButton { background-color: #ffff00; color: blue; font: bold }""" ) # 4. receive Signal @pyqtSlot() def receive_edit_idpw(self): self.btn1.setStyleSheet( """QPushButton { background-color: #ffff00; color: blue; font: bold }""" ) # 4. receive Signal(json_update_signal) @pyqtSlot() def receive_json_update(self): self.update_json() self.model = PandasModel(self.df) self.view.setModel(self.model) def update_json(self): result = ddld_json_to_df() self.ddld_dic = result[0] self.idpw_dic_lst_dic = result[1] self.df = result[2] self.emty_site_set = AddTable.item_set - set( self.df['website'].to_list()) #============= 3. Dataframe to original dic_lst_dic : https://hoy.kr/dXq62 # 1. 값 있는 것 df_to_dict = self.df.to_dict('split') _data_lst_lst = df_to_dict['data'] # 2. 값 없는 것 empty_site_lst = list(self.emty_site_set) empty_site_lst_lst = [[website, "", ""] for website in empty_site_lst] # 3. 1+2 data_lst_lst = _data_lst_lst + empty_site_lst_lst web_dic_lst_dic = dict() for web, id, pw in data_lst_lst: inner_dic = dict() inner_dic['id'] = id inner_dic['pw'] = pw if inner_dic['id'] != "": if web in web_dic_lst_dic.keys(): web_dic_lst_dic[web].append(inner_dic) # nd2 id/pw 이후 continue web_dic_lst_dic[web] = [inner_dic] # first id/pw continue web_dic_lst_dic[web] = [] # id/pw 가 없는 경우 #============ 4. dict to json # fulljs = r'C:\Ataxtech\ATT\Ver1.0\json\web.json' self.ddld_dic['idpw'] = web_dic_lst_dic with open(fulljs, 'w', encoding='utf-8') as fn: json.dump(self.ddld_dic, fn, ensure_ascii=False, indent=4)
class MainWindow(QMainWindow): def __init__(self, data): super().__init__() self.resize(400, 600) self.setWindowTitle('Logger Skeleton') self.statusBar().showMessage("Ready", 2000) # Make widgets #################################### self.tabs = QTabWidget(self) self.setCentralWidget(self.tabs) # Add tabs self.table_tab = QWidget(self) self.stats_tab = QWidget(self) self.tabs.addTab(self.table_tab, "Table") self.tabs.addTab(self.stats_tab, "Stats") # Table tab ########################################################### self.table_view = QTableView(self.table_tab) self.text_edit = QPlainTextEdit() self.btn_add_row = QPushButton("Add a row") #self.btn_remove_row = QPushButton("Remove selected rows") table_tab_vbox = QVBoxLayout() table_tab_vbox.addWidget(self.table_view) table_tab_vbox.addWidget(self.text_edit) table_tab_vbox.addWidget(self.btn_add_row) #table_tab_vbox.addWidget(self.btn_remove_row) self.table_tab.setLayout(table_tab_vbox) # Set model ####################################### my_model = DataQtModel(data, parent=self) # TODO: right use of "parent" ? # Proxy model ##################################### proxy_model = QSortFilterProxyModel(parent=self) # TODO: right use of "parent" ? proxy_model.setSourceModel(my_model) self.table_view.setModel(proxy_model) #self.table_view.setModel(my_model) # Set the view #################################### self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows) # Select the full row when a cell is selected (See http://doc.qt.io/qt-5/qabstractitemview.html#selectionBehavior-prop ) #self.table_view.setSelectionMode(QAbstractItemView.SingleSelection) # Set selection mode. See http://doc.qt.io/qt-5/qabstractitemview.html#selectionMode-prop self.table_view.setAlternatingRowColors(True) self.table_view.setSortingEnabled(True) self.table_view.setColumnWidth(0, 200) # TODO: automatically get the best width self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) # https://stackoverflow.com/q/17535563 self.table_view.setColumnHidden(COMMENT_COLUMN_INDEX, True) delegate = Delegate() self.table_view.setItemDelegate(delegate) # Set key shortcut ################################ # see https://stackoverflow.com/a/17631703 and http://doc.qt.io/qt-5/qaction.html#details # Add row action add_action = QAction(self.table_view) add_action.setShortcut(Qt.CTRL | Qt.Key_N) add_action.triggered.connect(self.add_row_btn_callback) self.table_view.addAction(add_action) # Delete action del_action = QAction(self.table_view) del_action.setShortcut(Qt.Key_Delete) del_action.triggered.connect(self.remove_row_callback) self.table_view.addAction(del_action) # Set QDataWidgetMapper ########################### self.mapper = QDataWidgetMapper() self.mapper.setModel(proxy_model) # WARNING: do not use `my_model` here otherwise the index mapping will be wrong! self.mapper.addMapping(self.text_edit, COMMENT_COLUMN_INDEX) self.mapper.toFirst() # TODO: is it a good idea ? self.table_view.selectionModel().selectionChanged.connect(self.update_selection) # Set slots ####################################### self.btn_add_row.clicked.connect(self.add_row_btn_callback) #self.btn_remove_row.clicked.connect(self.remove_row_callback) #self.table_view.setColumnHidden(1, True) # Stats tab ########################################################### # See https://matplotlib.org/examples/user_interfaces/embedding_in_qt5.html stats_tab_layout = QVBoxLayout(self.stats_tab) self.plot_canvas = PlotCanvas(data, self.stats_tab, width=5, height=4, dpi=100) stats_tab_layout.addWidget(self.plot_canvas) ################################################### #proxy_model.dataChanged.connect(plot_canvas.update_figure) #proxy_model.rowsInserted.connect(plot_canvas.update_figure) # TODO #proxy_model.rowsRemoved.connect(plot_canvas.update_figure) # TODO self.tabs.currentChanged.connect(self.updatePlot) # Update the stats plot when the tabs switch to the stats tab # Show ############################################ self.show() def update_selection(self, selected, deselected): index = self.table_view.selectionModel().currentIndex() self.mapper.setCurrentIndex(index.row()) print("Index: ", index.row()) def updatePlot(self, index): """ Parameters ---------- index Returns ------- """ if index == self.tabs.indexOf(self.stats_tab): self.plot_canvas.update_figure() def add_row_btn_callback(self): parent = QModelIndex() # More useful with e.g. tree structures #row_index = 0 # Insert new rows to the begining row_index = self.table_view.model().rowCount(parent) # Insert new rows to the end self.table_view.model().insertRows(row_index, 1, parent) def remove_row_callback(self): parent = QModelIndex() # More useful with e.g. tree structures # See http://doc.qt.io/qt-5/model-view-programming.html#handling-selections-in-item-views #current_index = self.table_view.selectionModel().currentIndex() #print("Current index:", current_index.row(), current_index.column()) selection_index_list = self.table_view.selectionModel().selectedRows() selected_row_list = [selection_index.row() for selection_index in selection_index_list] print("Current selection:", selected_row_list) #row_index = 0 # Remove the first row #row_index = self.table_view.model().rowCount(parent) - 1 # Remove the last row # WARNING: the list of rows to remove MUST be sorted in reverse order # otherwise the index of rows to remove may change at each iteration of the for loop! # TODO: there should be a lock mechanism to avoid model modifications from external sources while iterating this loop... # Or as a much simpler alternative, modify the ItemSelectionMode to forbid the non contiguous selection of rows and remove the following for loop for row_index in sorted(selected_row_list, reverse=True): # Remove rows one by one to allow the removql of non-contiguously selected rows (e.g. "rows 0, 2 and 3") success = self.table_view.model().removeRows(row_index, 1, parent) if not success: raise Exception("Unknown error...") # TODO
class MainWindow(QMainWindow): def __init__(self, data): super().__init__() self.resize(400, 600) self.setWindowTitle('Logger Skeleton') self.statusBar().showMessage("Ready", 2000) # Make widgets #################################### self.tabs = QTabWidget(self) self.setCentralWidget(self.tabs) # Add tabs self.table_tab = QWidget(self) self.stats_tab = QWidget(self) self.tabs.addTab(self.table_tab, "Table") self.tabs.addTab(self.stats_tab, "Stats") # Table tab ########################################################### self.table_view = QTableView(self.table_tab) self.text_edit = QPlainTextEdit() self.btn_add_row = QPushButton("Add a row") #self.btn_remove_row = QPushButton("Remove selected rows") table_tab_vbox = QVBoxLayout() table_tab_vbox.addWidget(self.table_view) table_tab_vbox.addWidget(self.text_edit) table_tab_vbox.addWidget(self.btn_add_row) #table_tab_vbox.addWidget(self.btn_remove_row) self.table_tab.setLayout(table_tab_vbox) # Set model ####################################### my_model = DataQtModel(data, parent=self) # TODO: right use of "parent" ? # Proxy model ##################################### proxy_model = QSortFilterProxyModel( parent=self) # TODO: right use of "parent" ? proxy_model.setSourceModel(my_model) self.table_view.setModel(proxy_model) #self.table_view.setModel(my_model) # Set the view #################################### self.table_view.setSelectionBehavior( QAbstractItemView.SelectRows ) # Select the full row when a cell is selected (See http://doc.qt.io/qt-5/qabstractitemview.html#selectionBehavior-prop ) #self.table_view.setSelectionMode(QAbstractItemView.SingleSelection) # Set selection mode. See http://doc.qt.io/qt-5/qabstractitemview.html#selectionMode-prop self.table_view.setAlternatingRowColors(True) self.table_view.setSortingEnabled(True) self.table_view.setColumnWidth( 0, 200) # TODO: automatically get the best width self.table_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) # https://stackoverflow.com/q/17535563 self.table_view.setColumnHidden(COMMENT_COLUMN_INDEX, True) delegate = Delegate() self.table_view.setItemDelegate(delegate) # Set key shortcut ################################ # see https://stackoverflow.com/a/17631703 and http://doc.qt.io/qt-5/qaction.html#details # Add row action add_action = QAction(self.table_view) add_action.setShortcut(Qt.CTRL | Qt.Key_N) add_action.triggered.connect(self.add_row_btn_callback) self.table_view.addAction(add_action) # Delete action del_action = QAction(self.table_view) del_action.setShortcut(Qt.Key_Delete) del_action.triggered.connect(self.remove_row_callback) self.table_view.addAction(del_action) # Set QDataWidgetMapper ########################### self.mapper = QDataWidgetMapper() self.mapper.setModel( proxy_model ) # WARNING: do not use `my_model` here otherwise the index mapping will be wrong! self.mapper.addMapping(self.text_edit, COMMENT_COLUMN_INDEX) self.mapper.toFirst() # TODO: is it a good idea ? self.table_view.selectionModel().selectionChanged.connect( self.update_selection) # Set slots ####################################### self.btn_add_row.clicked.connect(self.add_row_btn_callback) #self.btn_remove_row.clicked.connect(self.remove_row_callback) #self.table_view.setColumnHidden(1, True) # Stats tab ########################################################### # See https://matplotlib.org/examples/user_interfaces/embedding_in_qt5.html stats_tab_layout = QVBoxLayout(self.stats_tab) self.plot_canvas = PlotCanvas(data, self.stats_tab, width=5, height=4, dpi=100) stats_tab_layout.addWidget(self.plot_canvas) ################################################### #proxy_model.dataChanged.connect(plot_canvas.update_figure) #proxy_model.rowsInserted.connect(plot_canvas.update_figure) # TODO #proxy_model.rowsRemoved.connect(plot_canvas.update_figure) # TODO self.tabs.currentChanged.connect( self.updatePlot ) # Update the stats plot when the tabs switch to the stats tab # Show ############################################ self.show() def update_selection(self, selected, deselected): index = self.table_view.selectionModel().currentIndex() self.mapper.setCurrentIndex(index.row()) print("Index: ", index.row()) def updatePlot(self, index): """ Parameters ---------- index Returns ------- """ if index == self.tabs.indexOf(self.stats_tab): self.plot_canvas.update_figure() def add_row_btn_callback(self): parent = QModelIndex() # More useful with e.g. tree structures #row_index = 0 # Insert new rows to the begining row_index = self.table_view.model().rowCount( parent) # Insert new rows to the end self.table_view.model().insertRows(row_index, 1, parent) def remove_row_callback(self): parent = QModelIndex() # More useful with e.g. tree structures # See http://doc.qt.io/qt-5/model-view-programming.html#handling-selections-in-item-views #current_index = self.table_view.selectionModel().currentIndex() #print("Current index:", current_index.row(), current_index.column()) selection_index_list = self.table_view.selectionModel().selectedRows() selected_row_list = [ selection_index.row() for selection_index in selection_index_list ] print("Current selection:", selected_row_list) #row_index = 0 # Remove the first row #row_index = self.table_view.model().rowCount(parent) - 1 # Remove the last row # WARNING: the list of rows to remove MUST be sorted in reverse order # otherwise the index of rows to remove may change at each iteration of the for loop! # TODO: there should be a lock mechanism to avoid model modifications from external sources while iterating this loop... # Or as a much simpler alternative, modify the ItemSelectionMode to forbid the non contiguous selection of rows and remove the following for loop for row_index in sorted(selected_row_list, reverse=True): # Remove rows one by one to allow the removql of non-contiguously selected rows (e.g. "rows 0, 2 and 3") success = self.table_view.model().removeRows(row_index, 1, parent) if not success: raise Exception("Unknown error...") # TODO