From 5febee2aa39f7dfdc013283ad5a64d00c38f26fb Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Thu, 31 Oct 2019 17:13:21 +0800 Subject: [PATCH 1/9] [Add] new CodeEditor --- vnpy/trader/ui/editor.py | 213 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 vnpy/trader/ui/editor.py diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py new file mode 100644 index 00000000..f2cb1a37 --- /dev/null +++ b/vnpy/trader/ui/editor.py @@ -0,0 +1,213 @@ +from typing import Callable, Dict +from pathlib import Path + +from PyQt5 import QtWidgets, Qsci, QtGui + + +class CodeEditor(QtWidgets.QMainWindow): + """""" + + def __init__(self): + """""" + super().__init__() + + self.editors: [str, Qsci.QsciScintilla] = {} + + self.init_ui() + + def init_ui(self): + """""" + self.setWindowTitle("策略编辑器") + + self.init_menu() + self.init_central() + + def init_central(self): + """""" + self.tab = QtWidgets.QTabWidget() + self.path_label = QtWidgets.QLabel() + + vbox = QtWidgets.QVBoxLayout() + vbox.addWidget(self.tab) + vbox.addWidget(self.path_label) + + widget = QtWidgets.QWidget() + widget.setLayout(vbox) + + self.setCentralWidget(widget) + + def init_menu(self): + """""" + bar = self.menuBar() + + file_bar = bar.addMenu("文件") + self.add_menu_action(file_bar, "新建", self.new_file) + self.add_menu_action(file_bar, "打开", self.open_file) + self.add_menu_action(file_bar, "保存", self.save_file) + self.add_menu_action(file_bar, "另存为", self.save_file) + + edit_bar = bar.addMenu("编辑") + self.add_menu_action(edit_bar, "复制", self.copy) + self.add_menu_action(edit_bar, "粘贴", self.paste) + self.add_menu_action(edit_bar, "剪切", self.cut) + edit_bar.addSeparator() + self.add_menu_action(edit_bar, "查找", self.find) + self.add_menu_action(edit_bar, "替换", self.replace) + + help_bar = bar.addMenu("帮助") + self.add_menu_action(help_bar, "快捷键", self.show_shortcut) + + def add_menu_action( + self, + menu: QtWidgets.QMenu, + action_name: str, + func: Callable, + ): + """""" + action = QtWidgets.QAction(action_name, self) + action.triggered.connect(func) + menu.addAction(action) + + def new_editor(self): + """""" + # Create editor object + editor = Qsci.QsciScintilla() + + # Set editor font + font = QtGui.QFont() + font.setFamily('Consolas') + font.setFixedPitch(True) + font.setPointSize(10) + + editor.setFont(font) + editor.setMarginsFont(font) + + # Set margin for line numbers + font_metrics = QtGui.QFontMetrics(font) + editor.setMarginWidth(0, font_metrics.width("00000") + 6) + editor.setMarginLineNumbers(0, True) + editor.setMarginsBackgroundColor(QtGui.QColor("#cccccc")) + + # Set brace matching + editor.setBraceMatching(Qsci.QsciScintilla.SloppyBraceMatch) + + # Hide horizontal scroll bar + editor.SendScintilla(Qsci.QsciScintilla.SCI_SETHSCROLLBAR, 0) + + # Set current line color + editor.setCaretLineVisible(True) + editor.setCaretLineBackgroundColor(QtGui.QColor("#ffe4e4")) + + # Set Python lexer + lexer = Qsci.QsciLexerPython() + lexer.setDefaultFont(font) + editor.setLexer(lexer) + + # Add minimum editor size + editor.setMinimumSize(600, 450) + + # Enable auto complete + editor.setAutoCompletionSource(Qsci.QsciScintilla.AcsAll) + editor.setAutoCompletionThreshold(2) + editor.setAutoCompletionCaseSensitivity(False) + editor.setAutoCompletionReplaceWord(False) + + # Use space indentation + editor.setIndentationsUseTabs(False) + editor.setTabWidth(4) + editor.setIndentationGuides(True) + + # Enable folding + editor.setFolding(True) + + return editor + + def open_editor(self, file_path: str): + """""" + if file_path in self.editors: + editor = self.editors[file_path] + editor.show() + return + + editor = self.new_editor() + self.editors[file_path] = editor + + buf = open(file_path, encoding="UTF8").read() + editor.setText(buf) + + file_name = Path(file_path).name + i = self.tab.addTab(editor, file_name) + self.tab.setCurrentIndex(i) + + self.path_label.setText(file_path) + + def close_editor(self): + """""" + pass + + def open_file(self): + """""" + file_path, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "打开", "", "Python(*.py)") + + if file_path: + self.open_editor(file_path) + + def new_file(self): + """""" + file_path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "新建", "", "Python(*.py)") + + if file_path: + open(file_path, "a").close() + self.open_editor(file_path) + + def save_file(self): + """""" + pass + + def copy(self): + """""" + editor = self.get_active_editor() + editor.copy() + + def paste(self): + """""" + editor = self.get_active_editor() + editor.paste() + + def cut(self): + """""" + editor = self.get_active_editor() + editor.cut() + + def show_shortcut(self): + """""" + pass + + def find(self): + """""" + pass + + def replace(self): + """""" + pass + + def get_active_editor(self): + """""" + for editor in self.editors.values(): + if editor.isVisible(): + return editor + + +if __name__ == "__main__": + import sys + from vnpy.trader.ui import create_qapp + + app = create_qapp() + + editor = CodeEditor() + editor.showMaximized() + editor.open_editor(sys.argv[0]) + + app.exec_() From 990db038ee8e288898f8e255851041f70fb865db Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Thu, 31 Oct 2019 21:31:09 +0800 Subject: [PATCH 2/9] [Mod] add open/save file function --- vnpy/trader/ui/editor.py | 62 +++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py index f2cb1a37..91b6d682 100644 --- a/vnpy/trader/ui/editor.py +++ b/vnpy/trader/ui/editor.py @@ -6,12 +6,14 @@ from PyQt5 import QtWidgets, Qsci, QtGui class CodeEditor(QtWidgets.QMainWindow): """""" + NEW_FILE_NAME = "Untitled" def __init__(self): """""" super().__init__() - self.editors: [str, Qsci.QsciScintilla] = {} + self.new_file_count = 1 + self.editor_path_map: Dict[Qsci.QsciScintilla, str] = {} self.init_ui() @@ -122,20 +124,27 @@ class CodeEditor(QtWidgets.QMainWindow): return editor - def open_editor(self, file_path: str): + def open_editor(self, file_path: str = ""): """""" - if file_path in self.editors: - editor = self.editors[file_path] - editor.show() - return + # Show editor tab if file already opened + if file_path: + for editor, path in self.editor_path_map.items(): + if file_path == path: + editor.show() + return + # Otherwise create new editor editor = self.new_editor() - self.editors[file_path] = editor + self.editor_path_map[editor] = file_path - buf = open(file_path, encoding="UTF8").read() - editor.setText(buf) + if file_path: + buf = open(file_path, encoding="UTF8").read() + editor.setText(buf) + file_name = Path(file_path).name + else: + file_name = f"{self.NEW_FILE_NAME}-{self.new_file_count}" + self.new_file_count += 1 - file_name = Path(file_path).name i = self.tab.addTab(editor, file_name) self.tab.setCurrentIndex(i) @@ -155,31 +164,34 @@ class CodeEditor(QtWidgets.QMainWindow): def new_file(self): """""" - file_path, _ = QtWidgets.QFileDialog.getSaveFileName( - self, "新建", "", "Python(*.py)") - - if file_path: - open(file_path, "a").close() - self.open_editor(file_path) + self.open_editor("") def save_file(self): """""" - pass + editor = self.get_active_editor() + file_path = self.editor_path_map[editor] + + if self.NEW_FILE_NAME in file_path: + file_path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "保存", "", "Python(*.py)") + + if file_path: + self.editor_path_map[editor] = file_path + + with open(file_path, "w") as f: + f.write(editor.text()) def copy(self): """""" - editor = self.get_active_editor() - editor.copy() + self.get_active_editor().copy() def paste(self): """""" - editor = self.get_active_editor() - editor.paste() + self.get_active_editor.paste() def cut(self): """""" - editor = self.get_active_editor() - editor.cut() + self.get_active_editor.cut() def show_shortcut(self): """""" @@ -195,9 +207,7 @@ class CodeEditor(QtWidgets.QMainWindow): def get_active_editor(self): """""" - for editor in self.editors.values(): - if editor.isVisible(): - return editor + return self.tab.currentWidget() if __name__ == "__main__": From 31c7921c5726a09a95f3a2b1eff6ef2dba838ac2 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Thu, 31 Oct 2019 23:12:27 +0800 Subject: [PATCH 3/9] [Mod] complete general functions of editor --- vnpy/trader/ui/editor.py | 227 +++++++++++++++++++++++++++++++++++---- 1 file changed, 204 insertions(+), 23 deletions(-) diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py index 91b6d682..112030c3 100644 --- a/vnpy/trader/ui/editor.py +++ b/vnpy/trader/ui/editor.py @@ -12,7 +12,7 @@ class CodeEditor(QtWidgets.QMainWindow): """""" super().__init__() - self.new_file_count = 1 + self.new_file_count = 0 self.editor_path_map: Dict[Qsci.QsciScintilla, str] = {} self.init_ui() @@ -27,6 +27,8 @@ class CodeEditor(QtWidgets.QMainWindow): def init_central(self): """""" self.tab = QtWidgets.QTabWidget() + self.tab.currentChanged.connect(self.update_path_label) + self.path_label = QtWidgets.QLabel() vbox = QtWidgets.QVBoxLayout() @@ -42,34 +44,45 @@ class CodeEditor(QtWidgets.QMainWindow): """""" bar = self.menuBar() - file_bar = bar.addMenu("文件") - self.add_menu_action(file_bar, "新建", self.new_file) - self.add_menu_action(file_bar, "打开", self.open_file) - self.add_menu_action(file_bar, "保存", self.save_file) - self.add_menu_action(file_bar, "另存为", self.save_file) + file_menu = bar.addMenu("文件") + self.add_menu_action(file_menu, "新建", self.new_file, "Ctrl+N") + self.add_menu_action(file_menu, "打开", self.open_file, "Ctrl+O") + self.add_menu_action(file_menu, "保存", self.save_file, "Ctrl+S") + self.add_menu_action( + file_menu, + "另存为", + self.save_file_as, + "Ctrl+Shift+S" + ) + file_menu.addSeparator() + self.add_menu_action(file_menu, "退出", self.close) - edit_bar = bar.addMenu("编辑") - self.add_menu_action(edit_bar, "复制", self.copy) - self.add_menu_action(edit_bar, "粘贴", self.paste) - self.add_menu_action(edit_bar, "剪切", self.cut) - edit_bar.addSeparator() - self.add_menu_action(edit_bar, "查找", self.find) - self.add_menu_action(edit_bar, "替换", self.replace) + edit_menu = bar.addMenu("编辑") + self.add_menu_action(edit_menu, "复制", self.copy, "Ctrl+C") + self.add_menu_action(edit_menu, "粘贴", self.paste, "Ctrl+P") + self.add_menu_action(edit_menu, "剪切", self.cut, "Ctrl+X") + edit_menu.addSeparator() + self.add_menu_action(edit_menu, "查找", self.find, "Ctrl+F") + self.add_menu_action(edit_menu, "替换", self.replace, "Ctrl+H") - help_bar = bar.addMenu("帮助") - self.add_menu_action(help_bar, "快捷键", self.show_shortcut) + help_menu = bar.addMenu("帮助") def add_menu_action( self, menu: QtWidgets.QMenu, action_name: str, func: Callable, + shortcut: str = "", ): """""" action = QtWidgets.QAction(action_name, self) action.triggered.connect(func) menu.addAction(action) + if shortcut: + sequence = QtGui.QKeySequence(shortcut) + action.setShortcut(sequence) + def new_editor(self): """""" # Create editor object @@ -128,6 +141,8 @@ class CodeEditor(QtWidgets.QMainWindow): """""" # Show editor tab if file already opened if file_path: + file_path = str(Path(file_path)) + for editor, path in self.editor_path_map.items(): if file_path == path: editor.show() @@ -135,15 +150,18 @@ class CodeEditor(QtWidgets.QMainWindow): # Otherwise create new editor editor = self.new_editor() - self.editor_path_map[editor] = file_path if file_path: buf = open(file_path, encoding="UTF8").read() editor.setText(buf) file_name = Path(file_path).name + + self.editor_path_map[editor] = file_path else: - file_name = f"{self.NEW_FILE_NAME}-{self.new_file_count}" self.new_file_count += 1 + file_name = f"{self.NEW_FILE_NAME}-{self.new_file_count}" + + self.editor_path_map[editor] = file_name i = self.tab.addTab(editor, file_name) self.tab.setCurrentIndex(i) @@ -174,13 +192,33 @@ class CodeEditor(QtWidgets.QMainWindow): if self.NEW_FILE_NAME in file_path: file_path, _ = QtWidgets.QFileDialog.getSaveFileName( self, "保存", "", "Python(*.py)") + file_path = str(Path(file_path)) + self.save_editor_text(editor, file_path) + + def save_file_as(self): + """""" + editor = self.get_active_editor() + + file_path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "保存", "", "Python(*.py)") + + self.save_editor_text(editor, file_path) + + def save_editor_text(self, editor: Qsci.QsciScintilla, file_path: str): + """""" if file_path: self.editor_path_map[editor] = file_path + i = self.tab.currentIndex() + file_name = Path(file_path).name + self.tab.setTabText(i, file_name) + with open(file_path, "w") as f: f.write(editor.text()) + self.update_path_label() + def copy(self): """""" self.get_active_editor().copy() @@ -193,22 +231,165 @@ class CodeEditor(QtWidgets.QMainWindow): """""" self.get_active_editor.cut() - def show_shortcut(self): - """""" - pass - def find(self): """""" - pass + dialog = FindDialog( + self.get_active_editor(), + False + ) + dialog.exec_() def replace(self): """""" - pass + dialog = FindDialog( + self.get_active_editor(), + True + ) + dialog.exec_() def get_active_editor(self): """""" return self.tab.currentWidget() + def closeEvent(self, event): + """""" + for editor, path in self.editor_path_map.items(): + i = QtWidgets.QMessageBox.question( + self, + "退出保存", + f"是否要保存{path}?", + QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Save + ) + + if i == QtWidgets.QMessageBox.Save: + if self.NEW_FILE_NAME in path: + path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "保存", "", "Python(*.py)") + + if path: + self.save_editor_text(editor, path) + elif i == QtWidgets.QMessageBox.Cancel: + break + + event.accept() + + def update_path_label(self): + """""" + editor = self.get_active_editor() + path = self.editor_path_map[editor] + self.path_label.setText(path) + + +class FindDialog(QtWidgets.QDialog): + """""" + + def __init__(self, editor: Qsci.QsciScintilla, replace: bool = False): + """""" + super().__init__() + + self.editor = editor + self.replace = replace + + self.text = "" + self.case_sensitive = False + self.whole_word = False + self.selection = False + + self.init_ui() + + def init_ui(self): + """""" + find_label = QtWidgets.QLabel("查找") + replace_label = QtWidgets.QLabel("替换") + + self.find_line = QtWidgets.QLineEdit() + self.replace_line = QtWidgets.QLineEdit() + + self.case_check = QtWidgets.QCheckBox("大小写") + self.whole_check = QtWidgets.QCheckBox("全词匹配") + self.selection_check = QtWidgets.QCheckBox("选中区域") + + find_button = QtWidgets.QPushButton("查找") + find_button.clicked.connect(self.find_text) + + replace_button = QtWidgets.QPushButton("替换") + replace_button.clicked.connect(self.replace_text) + + check_hbox = QtWidgets.QHBoxLayout() + check_hbox.addWidget(self.case_check) + check_hbox.addStretch() + check_hbox.addWidget(self.whole_check) + check_hbox.addStretch() + check_hbox.addWidget(self.selection_check) + check_hbox.addStretch() + + button_hbox = QtWidgets.QHBoxLayout() + button_hbox.addWidget(find_button) + button_hbox.addWidget(replace_button) + + form = QtWidgets.QFormLayout() + form.addRow(find_label, self.find_line) + form.addRow(replace_label, self.replace_line) + form.addRow(check_hbox) + form.addRow(button_hbox) + + self.setLayout(form) + + if self.replace: + self.setWindowTitle("替换") + else: + self.setWindowTitle("查找") + replace_label.setVisible(False) + self.replace_line.setVisible(False) + replace_button.setVisible(False) + + def find_text(self): + """""" + new_text = self.find_line.text() + new_case_sensitive = self.case_check.isChecked() + new_whole_word = self.whole_check.isChecked() + new_selection = self.selection_check.isChecked() + + if ( + new_text == self.text + and new_case_sensitive == self.case_sensitive + and new_whole_word == self.whole_word + and new_selection == self.selection + ): + self.editor.findNext() + return + + self.text = new_text + self.case_sensitive = new_case_sensitive + self.whole_word = new_whole_word + self.selection = new_selection + + if not self.selection: + self.editor.findFirst( + self.text, + False, + self.case_sensitive, + self.whole_word, + False, + show=True + ) + else: + self.editor.findFirstInSelection( + self.text, + False, + self.case_sensitive, + self.whole_word, + False, + show=True + ) + + def replace_text(self): + """""" + new_text = self.replace_line.text() + + self.editor.replace(new_text) + if __name__ == "__main__": import sys From 21f525ac9662f4a4cf443ca227acc5869b1f5ed2 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Fri, 1 Nov 2019 09:19:17 +0800 Subject: [PATCH 4/9] [Mod] improve find/replace function --- vnpy/trader/ui/editor.py | 124 +++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py index 112030c3..6f4d9122 100644 --- a/vnpy/trader/ui/editor.py +++ b/vnpy/trader/ui/editor.py @@ -45,8 +45,8 @@ class CodeEditor(QtWidgets.QMainWindow): bar = self.menuBar() file_menu = bar.addMenu("文件") - self.add_menu_action(file_menu, "新建", self.new_file, "Ctrl+N") - self.add_menu_action(file_menu, "打开", self.open_file, "Ctrl+O") + self.add_menu_action(file_menu, "新建文件", self.new_file, "Ctrl+N") + self.add_menu_action(file_menu, "打开文件", self.open_file, "Ctrl+O") self.add_menu_action(file_menu, "保存", self.save_file, "Ctrl+S") self.add_menu_action( file_menu, @@ -58,6 +58,9 @@ class CodeEditor(QtWidgets.QMainWindow): self.add_menu_action(file_menu, "退出", self.close) edit_menu = bar.addMenu("编辑") + self.add_menu_action(edit_menu, "撤销", self.undo, "Ctrl+Z") + self.add_menu_action(edit_menu, "恢复", self.redo, "Ctrl+Y") + edit_menu.addSeparator() self.add_menu_action(edit_menu, "复制", self.copy, "Ctrl+C") self.add_menu_action(edit_menu, "粘贴", self.paste, "Ctrl+P") self.add_menu_action(edit_menu, "剪切", self.cut, "Ctrl+X") @@ -65,8 +68,6 @@ class CodeEditor(QtWidgets.QMainWindow): self.add_menu_action(edit_menu, "查找", self.find, "Ctrl+F") self.add_menu_action(edit_menu, "替换", self.replace, "Ctrl+H") - help_menu = bar.addMenu("帮助") - def add_menu_action( self, menu: QtWidgets.QMenu, @@ -175,7 +176,7 @@ class CodeEditor(QtWidgets.QMainWindow): def open_file(self): """""" file_path, _ = QtWidgets.QFileDialog.getOpenFileName( - self, "打开", "", "Python(*.py)") + self, "打开文件", "", "Python(*.py)") if file_path: self.open_editor(file_path) @@ -225,11 +226,19 @@ class CodeEditor(QtWidgets.QMainWindow): def paste(self): """""" - self.get_active_editor.paste() + self.get_active_editor().paste() + + def undo(self): + """""" + self.get_active_editor().undo() + + def redo(self): + """""" + self.get_active_editor().redo() def cut(self): """""" - self.get_active_editor.cut() + self.get_active_editor().cut() def find(self): """""" @@ -284,17 +293,17 @@ class CodeEditor(QtWidgets.QMainWindow): class FindDialog(QtWidgets.QDialog): """""" - def __init__(self, editor: Qsci.QsciScintilla, replace: bool = False): + def __init__( + self, + editor: Qsci.QsciScintilla, + show_replace: bool = False + ): """""" super().__init__() - self.editor = editor - self.replace = replace - - self.text = "" - self.case_sensitive = False - self.whole_word = False - self.selection = False + self.editor: Qsci.QsciScintilla = editor + self.show_replace: bool = show_replace + self.new_task: bool = True self.init_ui() @@ -303,18 +312,28 @@ class FindDialog(QtWidgets.QDialog): find_label = QtWidgets.QLabel("查找") replace_label = QtWidgets.QLabel("替换") - self.find_line = QtWidgets.QLineEdit() + selected_text = self.editor.selectedText() + self.find_line = QtWidgets.QLineEdit(selected_text) + self.find_line.textChanged.connect(self.reset_task) + self.replace_line = QtWidgets.QLineEdit() self.case_check = QtWidgets.QCheckBox("大小写") + self.case_check.setChecked(True) + self.case_check.stateChanged.connect(self.reset_task) + self.whole_check = QtWidgets.QCheckBox("全词匹配") + self.whole_check.stateChanged.connect(self.reset_task) + self.selection_check = QtWidgets.QCheckBox("选中区域") + self.selection_check.stateChanged.connect(self.reset_task) find_button = QtWidgets.QPushButton("查找") find_button.clicked.connect(self.find_text) - replace_button = QtWidgets.QPushButton("替换") - replace_button.clicked.connect(self.replace_text) + self.replace_button = QtWidgets.QPushButton("替换") + self.replace_button.clicked.connect(self.replace_text) + self.replace_button.setEnabled(False) check_hbox = QtWidgets.QHBoxLayout() check_hbox.addWidget(self.case_check) @@ -326,7 +345,7 @@ class FindDialog(QtWidgets.QDialog): button_hbox = QtWidgets.QHBoxLayout() button_hbox.addWidget(find_button) - button_hbox.addWidget(replace_button) + button_hbox.addWidget(self.replace_button) form = QtWidgets.QFormLayout() form.addRow(find_label, self.find_line) @@ -336,59 +355,66 @@ class FindDialog(QtWidgets.QDialog): self.setLayout(form) - if self.replace: + if self.show_replace: self.setWindowTitle("替换") else: self.setWindowTitle("查找") + replace_label.setVisible(False) self.replace_line.setVisible(False) - replace_button.setVisible(False) + self.replace_button.setVisible(False) def find_text(self): """""" - new_text = self.find_line.text() - new_case_sensitive = self.case_check.isChecked() - new_whole_word = self.whole_check.isChecked() - new_selection = self.selection_check.isChecked() + if not self.new_task: + result = self.editor.findNext() - if ( - new_text == self.text - and new_case_sensitive == self.case_sensitive - and new_whole_word == self.whole_word - and new_selection == self.selection - ): - self.editor.findNext() - return + if result: + self.new_task = False + self.replace_button.setEnabled(True) + return + else: + self.new_task = True - self.text = new_text - self.case_sensitive = new_case_sensitive - self.whole_word = new_whole_word - self.selection = new_selection + self.editor.cancelFind() - if not self.selection: - self.editor.findFirst( - self.text, + if not self.selection_check.isChecked(): + result = self.editor.findFirst( + self.find_line.text(), False, - self.case_sensitive, - self.whole_word, + self.case_check.isChecked(), + self.whole_check.isChecked(), False, - show=True + line=1, + index=1 ) + else: - self.editor.findFirstInSelection( - self.text, + result = self.editor.findFirstInSelection( + self.find_line.text(), False, - self.case_sensitive, - self.whole_word, - False, - show=True + self.case_check.isChecked(), + self.whole_check.isChecked(), + False ) + if result: + self.new_task = False + self.replace_button.setEnabled(True) + else: + self.new_task = True + def replace_text(self): """""" new_text = self.replace_line.text() self.editor.replace(new_text) + self.editor.findNext() + + def reset_task(self): + """""" + self.new_task = True + self.replace_button.setEnabled(False) if __name__ == "__main__": From 7d940f74f6c285ed3db41eb107d9a2d7975b4045 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Fri, 1 Nov 2019 09:35:35 +0800 Subject: [PATCH 5/9] [Add] close editor function --- vnpy/trader/ui/editor.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py index 6f4d9122..71fa8578 100644 --- a/vnpy/trader/ui/editor.py +++ b/vnpy/trader/ui/editor.py @@ -47,6 +47,8 @@ class CodeEditor(QtWidgets.QMainWindow): file_menu = bar.addMenu("文件") self.add_menu_action(file_menu, "新建文件", self.new_file, "Ctrl+N") self.add_menu_action(file_menu, "打开文件", self.open_file, "Ctrl+O") + self.add_menu_action(file_menu, "关闭文件", self.close_editor, "Ctrl+W") + file_menu.addSeparator() self.add_menu_action(file_menu, "保存", self.save_file, "Ctrl+S") self.add_menu_action( file_menu, @@ -171,7 +173,19 @@ class CodeEditor(QtWidgets.QMainWindow): def close_editor(self): """""" - pass + i = self.tab.currentIndex() + + # Close editor if last file closed + if not i: + self.close() + # Otherwise only close current tab + else: + self.save_file() + + editor = self.get_active_editor() + self.editor_path_map.pop(editor) + + self.tab.removeTab(i) def open_file(self): """""" @@ -193,7 +207,6 @@ class CodeEditor(QtWidgets.QMainWindow): if self.NEW_FILE_NAME in file_path: file_path, _ = QtWidgets.QFileDialog.getSaveFileName( self, "保存", "", "Python(*.py)") - file_path = str(Path(file_path)) self.save_editor_text(editor, file_path) @@ -215,7 +228,7 @@ class CodeEditor(QtWidgets.QMainWindow): file_name = Path(file_path).name self.tab.setTabText(i, file_name) - with open(file_path, "w") as f: + with open(file_path, "w", encoding="UTF8") as f: f.write(editor.text()) self.update_path_label() @@ -418,13 +431,12 @@ class FindDialog(QtWidgets.QDialog): if __name__ == "__main__": - import sys from vnpy.trader.ui import create_qapp app = create_qapp() editor = CodeEditor() + editor.open_editor() editor.showMaximized() - editor.open_editor(sys.argv[0]) app.exec_() From 99fbebbbdbb624f24abc8081cb5ce654a016c62a Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Fri, 1 Nov 2019 09:48:10 +0800 Subject: [PATCH 6/9] [Mod] add CodeEditor function to main window --- vnpy/trader/ui/editor.py | 12 +++++++++--- vnpy/trader/ui/ico/editor.ico | Bin 67646 -> 67646 bytes vnpy/trader/ui/mainwindow.py | 13 +++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py index 71fa8578..42d0ae38 100644 --- a/vnpy/trader/ui/editor.py +++ b/vnpy/trader/ui/editor.py @@ -8,7 +8,7 @@ class CodeEditor(QtWidgets.QMainWindow): """""" NEW_FILE_NAME = "Untitled" - def __init__(self): + def __init__(self, main_engine=None, event_engine=None): """""" super().__init__() @@ -296,6 +296,13 @@ class CodeEditor(QtWidgets.QMainWindow): event.accept() + def show(self): + """""" + if not self.tab.count(): + self.open_editor() + + self.showMaximized() + def update_path_label(self): """""" editor = self.get_active_editor() @@ -436,7 +443,6 @@ if __name__ == "__main__": app = create_qapp() editor = CodeEditor() - editor.open_editor() - editor.showMaximized() + editor.show() app.exec_() diff --git a/vnpy/trader/ui/ico/editor.ico b/vnpy/trader/ui/ico/editor.ico index e52273768ba59b89403ae24282c21560ef00e4e0..c0f1ae0031339c12158a90aae26484e866d9c21f 100644 GIT binary patch literal 67646 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 AM*si- literal 67646 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 diff --git a/vnpy/trader/ui/mainwindow.py b/vnpy/trader/ui/mainwindow.py index 3f293401..1e95e097 100644 --- a/vnpy/trader/ui/mainwindow.py +++ b/vnpy/trader/ui/mainwindow.py @@ -24,6 +24,7 @@ from .widget import ( AboutDialog, GlobalDialog ) +from .editor import CodeEditor from ..engine import MainEngine from ..utility import get_icon_path, TRADER_DIR @@ -138,6 +139,18 @@ class MainWindow(QtWidgets.QMainWindow): partial(self.open_widget, ContractManager, "contract") ) + self.add_menu_action( + help_menu, + "代码编辑", + "editor.ico", + partial(self.open_widget, CodeEditor, "editor"), + ) + self.add_toolbar_action( + "代码编辑", + "editor.ico", + partial(self.open_widget, CodeEditor, "editor") + ) + self.add_menu_action( help_menu, "还原窗口", "restore.ico", self.restore_window_setting ) From 4f82f061ec677823c14144958782bab43fa05097 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Fri, 1 Nov 2019 10:12:37 +0800 Subject: [PATCH 7/9] [Mod] make CodeEditor singleton --- vnpy/trader/ui/editor.py | 10 ++++++++-- vnpy/trader/ui/ico/editor.ico | Bin 67646 -> 67646 bytes vnpy/trader/ui/mainwindow.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/vnpy/trader/ui/editor.py b/vnpy/trader/ui/editor.py index 42d0ae38..050be6b9 100644 --- a/vnpy/trader/ui/editor.py +++ b/vnpy/trader/ui/editor.py @@ -8,6 +8,14 @@ class CodeEditor(QtWidgets.QMainWindow): """""" NEW_FILE_NAME = "Untitled" + _instance = None + + def __new__(cls, *args, **kwargs): + """""" + if not cls._instance: + cls._instance = QtWidgets.QMainWindow.__new__(cls, *args, **kwargs) + return cls._instance + def __init__(self, main_engine=None, event_engine=None): """""" super().__init__() @@ -441,8 +449,6 @@ if __name__ == "__main__": from vnpy.trader.ui import create_qapp app = create_qapp() - editor = CodeEditor() editor.show() - app.exec_() diff --git a/vnpy/trader/ui/ico/editor.ico b/vnpy/trader/ui/ico/editor.ico index c0f1ae0031339c12158a90aae26484e866d9c21f..e886970f14dcebae5d9e8784e40cff2dac074df9 100644 GIT binary patch literal 67646 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 literal 67646 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 AM*si- diff --git a/vnpy/trader/ui/mainwindow.py b/vnpy/trader/ui/mainwindow.py index 1e95e097..2cad1a7b 100644 --- a/vnpy/trader/ui/mainwindow.py +++ b/vnpy/trader/ui/mainwindow.py @@ -143,7 +143,7 @@ class MainWindow(QtWidgets.QMainWindow): help_menu, "代码编辑", "editor.ico", - partial(self.open_widget, CodeEditor, "editor"), + partial(self.open_widget, CodeEditor, "editor") ) self.add_toolbar_action( "代码编辑", From efedb60a85f90ac2b59fb639c0bcba2b455e5f88 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Fri, 1 Nov 2019 10:38:19 +0800 Subject: [PATCH 8/9] [Add] edit strategy code directly in cta-backtester --- vnpy/app/cta_backtester/engine.py | 13 +++++++++ vnpy/app/cta_backtester/ui/widget.py | 43 ++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/vnpy/app/cta_backtester/engine.py b/vnpy/app/cta_backtester/engine.py index 4214baa3..2c47f4dc 100644 --- a/vnpy/app/cta_backtester/engine.py +++ b/vnpy/app/cta_backtester/engine.py @@ -4,6 +4,7 @@ import traceback from datetime import datetime from threading import Thread from pathlib import Path +from inspect import getfile from vnpy.event import Event, EventEngine from vnpy.trader.engine import BaseEngine, MainEngine @@ -116,6 +117,12 @@ class BacktesterEngine(BaseEngine): msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}" self.write_log(msg) + def reload_strategy_class(self): + """""" + self.classes.clear() + self.load_strategy_class() + self.write_log("策略文件重载刷新完成") + def get_strategy_class_names(self): """""" return list(self.classes.keys()) @@ -425,3 +432,9 @@ class BacktesterEngine(BaseEngine): def get_history_data(self): """""" return self.backtesting_engine.history_data + + def get_strategy_class_file(self, class_name: str): + """""" + strategy_class = self.classes[class_name] + file_path = getfile(strategy_class) + return file_path diff --git a/vnpy/app/cta_backtester/ui/widget.py b/vnpy/app/cta_backtester/ui/widget.py index 87d1ad0a..1aba2e23 100644 --- a/vnpy/app/cta_backtester/ui/widget.py +++ b/vnpy/app/cta_backtester/ui/widget.py @@ -13,6 +13,7 @@ from vnpy.trader.constant import Interval, Direction from vnpy.trader.engine import MainEngine from vnpy.trader.ui import QtCore, QtWidgets, QtGui from vnpy.trader.ui.widget import BaseMonitor, BaseCell, DirectionCell, EnumCell +from vnpy.trader.ui.editor import CodeEditor from vnpy.event import Event, EventEngine from vnpy.chart import ChartWidget, CandleItem, VolumeItem @@ -117,6 +118,12 @@ class BacktesterManager(QtWidgets.QWidget): self.candle_button.clicked.connect(self.show_candle_chart) self.candle_button.setEnabled(False) + edit_button = QtWidgets.QPushButton("代码编辑") + edit_button.clicked.connect(self.edit_strategy_code) + + reload_button = QtWidgets.QPushButton("策略重载") + reload_button.clicked.connect(self.reload_strategy_class) + for button in [ backtesting_button, optimization_button, @@ -125,7 +132,9 @@ class BacktesterManager(QtWidgets.QWidget): self.order_button, self.trade_button, self.daily_button, - self.candle_button + self.candle_button, + edit_button, + reload_button ]: button.setFixedHeight(button.sizeHint().height() * 2) @@ -142,18 +151,24 @@ class BacktesterManager(QtWidgets.QWidget): form.addRow("回测资金", self.capital_line) form.addRow("合约模式", self.inverse_combo) + result_grid = QtWidgets.QGridLayout() + result_grid.addWidget(self.trade_button, 0, 0) + result_grid.addWidget(self.order_button, 0, 1) + result_grid.addWidget(self.daily_button, 1, 0) + result_grid.addWidget(self.candle_button, 1, 1) + left_vbox = QtWidgets.QVBoxLayout() left_vbox.addLayout(form) left_vbox.addWidget(backtesting_button) left_vbox.addWidget(downloading_button) left_vbox.addStretch() - left_vbox.addWidget(self.trade_button) - left_vbox.addWidget(self.order_button) - left_vbox.addWidget(self.daily_button) - left_vbox.addWidget(self.candle_button) + left_vbox.addLayout(result_grid) left_vbox.addStretch() left_vbox.addWidget(optimization_button) left_vbox.addWidget(self.result_button) + left_vbox.addStretch() + left_vbox.addWidget(edit_button) + left_vbox.addWidget(reload_button) # Result part self.statistics_monitor = StatisticsMonitor() @@ -197,6 +212,9 @@ class BacktesterManager(QtWidgets.QWidget): hbox.addWidget(self.chart) self.setLayout(hbox) + # Code Editor + self.editor = CodeEditor(self.main_engine, self.event_engine) + def register_event(self): """""" self.signal_log.connect(self.process_log_event) @@ -403,6 +421,21 @@ class BacktesterManager(QtWidgets.QWidget): self.candle_dialog.exec_() + def edit_strategy_code(self): + """""" + class_name = self.class_combo.currentText() + file_path = self.backtester_engine.get_strategy_class_file(class_name) + + self.editor.open_editor(file_path) + self.editor.show() + + def reload_strategy_class(self): + """""" + self.backtester_engine.reload_strategy_class() + + self.class_combo.clear() + self.init_strategy_settings() + def show(self): """""" self.showMaximized() From b18cb1baa48e1048508365a019685d542694b4cb Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Fri, 1 Nov 2019 10:39:32 +0800 Subject: [PATCH 9/9] [Mod] add QScintilla into requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 3d7a0271..fd5f8f45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ ibapi deap pyzmq wmi +QScintilla \ No newline at end of file