From 73ed5ec9d88448e308cb08930dcb9f6908058d46 Mon Sep 17 00:00:00 2001 From: Li Yuanhe Date: Wed, 12 Oct 2022 14:34:25 +0800 Subject: [PATCH] Solve Resizing Problem; Solve High DPI Problem; Switch to PyInstaller --- .gitignore | 4 +- Erying_Eq.spec | 51 +++++++++++++++++ Eyring_Eq.iml | 2 + Eyring_Eq.py | 116 ++++++++++++++++++-------------------- Eyring_Eq.spec | 51 +++++++++++++++++ Python_Lib/My_Lib_PyQt.py | 24 +++++++- pyinstaller.py | 103 +++++++++++++++++++++++++++++++++ 7 files changed, 287 insertions(+), 64 deletions(-) create mode 100644 Erying_Eq.spec create mode 100644 Eyring_Eq.spec create mode 100644 pyinstaller.py diff --git a/.gitignore b/.gitignore index 24ad4ce..0999a12 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /UI/Eyring_Eq.psd /.idea/ /build/ -*.pyc \ No newline at end of file +*.pyc +/Obsolete Cx_Freeze/ +/dist/ diff --git a/Erying_Eq.spec b/Erying_Eq.spec new file mode 100644 index 0000000..5e96770 --- /dev/null +++ b/Erying_Eq.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['Erying_Eq.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='Erying_Eq', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['UI\\Erying_Eq.ico'], +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='Erying_Eq', +) diff --git a/Eyring_Eq.iml b/Eyring_Eq.iml index d0f208f..33c9e17 100644 --- a/Eyring_Eq.iml +++ b/Eyring_Eq.iml @@ -4,6 +4,8 @@ + + diff --git a/Eyring_Eq.py b/Eyring_Eq.py index 66ae88b..956ca6e 100644 --- a/Eyring_Eq.py +++ b/Eyring_Eq.py @@ -25,29 +25,25 @@ if not Qt.QApplication.instance(): Application = Qt.QApplication(sys.argv) - - if platform.system() == 'Windows': import ctypes - set_Windows_scaling_factor_env_var() + + Windows_DPI_ratio, PyQt_scaling_ratio = set_Windows_scaling_factor_env_var() del Application Application = Qt.QApplication(sys.argv) - APPID = 'LYH.EyringEq.0.1' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APPID) - Application.setWindowIcon(Qt.QIcon('UI/Eyring_Eq.png')) print('If there is a warning above starts with "libpng", ignore that.') - if __name__ == '__main__': pyqt_ui_compile('Eyring_Eq.py') from UI.Eyring_Eq import Ui_Eyring_Eq def evaluate_expression(expression: str): - expression = expression.replace(' × 10^','E') + expression = expression.replace(' × 10^', 'E') try: ret = eval(expression) if is_float(ret): @@ -60,33 +56,33 @@ class myWidget(Ui_Eyring_Eq, Qt.QWidget, Qt_Widget_Common_Functions): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) - self.setFixedWidth(460) - self.setFixedHeight(336) + self.setMinimumWidth(460) + self.setMinimumHeight(336) connect_once(self.unimolecular_radioButton, self.unimolecular_selected) connect_once(self.bimolecular_AA_radioButton, self.bimolecular_AA_selected) connect_once(self.bimolecular_AB_radioButton, self.bimolecular_AB_selected) connect_once(self.reset_all_pushButton, self.reset_all) connect_once(self.calculate_pushButton, self.calc) - connect_once(self.conversion_lineEdit,self.check_fill_status) - connect_once(self.total_time_lineEdit,self.check_fill_status) - connect_once(self.temp_lineEdit,self.check_fill_status) - connect_once(self.G_neq_lineEdit,self.check_fill_status) - connect_once(self.kTST_lineEdit,self.check_fill_status) - connect_once(self.conc2_lineEdit,self.check_fill_status) - connect_once(self.conc1_lineEdit,self.check_fill_status) - connect_once(self.sigma_lineEdit,self.check_fill_status) - connect_once(self.unimolecular_radioButton,self.check_fill_status) - connect_once(self.bimolecular_AA_radioButton,self.check_fill_status) - connect_once(self.bimolecular_AB_radioButton,self.check_fill_status) - connect_once(self.total_time_lineEdit,self.smart_display_total_time) - - connect_once(self.temp_lineEdit,self.clear_kTST) - connect_once(self.G_neq_lineEdit,self.clear_kTST) - connect_once(self.sigma_lineEdit,self.clear_kTST) - connect_once(self.unimolecular_radioButton,self.clear_kTST) - connect_once(self.bimolecular_AA_radioButton,self.clear_kTST) - connect_once(self.bimolecular_AB_radioButton,self.clear_kTST) + connect_once(self.conversion_lineEdit, self.check_fill_status) + connect_once(self.total_time_lineEdit, self.check_fill_status) + connect_once(self.temp_lineEdit, self.check_fill_status) + connect_once(self.G_neq_lineEdit, self.check_fill_status) + connect_once(self.kTST_lineEdit, self.check_fill_status) + connect_once(self.conc2_lineEdit, self.check_fill_status) + connect_once(self.conc1_lineEdit, self.check_fill_status) + connect_once(self.sigma_lineEdit, self.check_fill_status) + connect_once(self.unimolecular_radioButton, self.check_fill_status) + connect_once(self.bimolecular_AA_radioButton, self.check_fill_status) + connect_once(self.bimolecular_AB_radioButton, self.check_fill_status) + connect_once(self.total_time_lineEdit, self.smart_display_total_time) + + connect_once(self.temp_lineEdit, self.clear_kTST) + connect_once(self.G_neq_lineEdit, self.clear_kTST) + connect_once(self.sigma_lineEdit, self.clear_kTST) + connect_once(self.unimolecular_radioButton, self.clear_kTST) + connect_once(self.bimolecular_AA_radioButton, self.clear_kTST) + connect_once(self.bimolecular_AB_radioButton, self.clear_kTST) self.clear_conc1_pushButton.hide() self.clear_conc2_pushButton.hide() @@ -97,12 +93,12 @@ def __init__(self): self.clear_conv_pushButton.hide() self.clear_sigma_pushButton.hide() - self.kTST_is_calculated_marker = "⠀" # an unicode blank to tell the program that the kTST is calculated, instead of user input. + self.kTST_is_calculated_marker = "⠀" # an unicode blank to tell the program that the kTST is calculated, instead of user input. self.unimolecular_selected() self.check_fill_status() self.show() self.center_the_widget() - + self.resize(self.minimumSizeHint()) # def clear_G(self): # self.G_neq_lineEdit.setText("") @@ -148,42 +144,41 @@ def clear_kTST(self): self.kTST_lineEdit.setText("") def smart_display_total_time(self): - if self.data["t"] and self.data["t"]>60: - self.total_time_display_label.setText("= "+smart_print_time(self.data["t"])) + if self.data["t"] and self.data["t"] > 60: + self.total_time_display_label.setText("= " + smart_print_time(self.data["t"])) else: self.total_time_display_label.setText("") - def set_kTST_line_edit(self,kTST): + def set_kTST_line_edit(self, kTST): if self.unimolecular_radioButton.isChecked(): self.kTST_lineEdit.setText(self.kTST_is_calculated_marker + smart_format_float(kTST)) else: - self.kTST_lineEdit.setText(self.kTST_is_calculated_marker + smart_format_float(kTST*1000)) - + self.kTST_lineEdit.setText(self.kTST_is_calculated_marker + smart_format_float(kTST * 1000)) def check_fill_status(self): self.data = {} self.data["G"] = evaluate_expression(self.G_neq_lineEdit.text()) if self.data['G'] is not None: - self.data['G']*=1000 # kJ-> J + self.data['G'] *= 1000 # kJ-> J self.data["T"] = evaluate_expression(self.temp_lineEdit.text()) if self.data['T'] is not None: - self.data['T']+=273 + self.data['T'] += 273 self.data["c1"] = evaluate_expression(self.conc1_lineEdit.text()) if self.data['c1'] is not None: - self.data['c1']*=1000 #mol/L -> mol/m^3 + self.data['c1'] *= 1000 # mol/L -> mol/m^3 self.data["c2"] = evaluate_expression(self.conc2_lineEdit.text()) if self.data['c2'] is not None: - self.data['c2']*=1000 #mol/L -> mol/m^3 + self.data['c2'] *= 1000 # mol/L -> mol/m^3 self.data["t"] = evaluate_expression(self.total_time_lineEdit.text()) self.data["conv"] = evaluate_expression(self.conversion_lineEdit.text()) if self.data['conv'] is not None: - self.data['conv']/=100 # percent to number + self.data['conv'] /= 100 # percent to number if self.kTST_lineEdit.text().startswith(self.kTST_is_calculated_marker): self.data['kTST'] = None else: self.data['kTST'] = evaluate_expression(self.kTST_lineEdit.text()) if not self.unimolecular_radioButton.isChecked() and self.data['kTST']: - self.data['kTST']/=1000 # L/mol·s --> m3/mol·s + self.data['kTST'] /= 1000 # L/mol·s --> m3/mol·s self.data["σ"] = evaluate_expression(self.sigma_lineEdit.text()) self.is_None = set([key for key, value in self.data.items() if value is None]) if self.unimolecular_radioButton.isChecked(): @@ -197,18 +192,17 @@ def check_fill_status(self): self.is_None.remove("c2") except KeyError: pass - allowed_missing_situations = [["G",'kTST'], - ["T",'kTST'], - ["t",'kTST'], - ["conv",'kTST'], - ["G",'t'], - ["G",'conv'], - ["T",'t'], - ["T",'conv']] + allowed_missing_situations = [["G", 'kTST'], + ["T", 'kTST'], + ["t", 'kTST'], + ["conv", 'kTST'], + ["G", 't'], + ["G", 'conv'], + ["T", 't'], + ["T", 'conv']] allowed = any([set(x) == self.is_None for x in allowed_missing_situations]) self.calculate_pushButton.setEnabled(allowed) - def calc(self): if self.unimolecular_radioButton.isChecked(): Δn = 0 @@ -239,24 +233,24 @@ def conv_from_kinetics(kTST, time, conc1, conc2=None): Δn = 1 def k_from_kinetics(conv, time, conc1, conc2): - if conc1==conc2: + if conc1 == conc2: return second_order_k_TST_A_plus_A(conv, time, conc1) else: return second_order_k_TST_A_plus_B(conv, time, conc1, conc2) def t_from_kinetics(kTST, conv, conc1, conc2): - if conc1==conc2: + if conc1 == conc2: return second_order_reaction_time_A_plus_A(kTST, conv, conc1) else: return second_order_reaction_time_A_plus_B(kTST, conv, conc1, conc2) def conv_from_kinetics(kTST, time, conc1, conc2): - if conc1==conc2: + if conc1 == conc2: return second_order_conv_A_plus_A(kTST, time, conc1) else: return second_order_conv_A_plus_B(kTST, time, conc1, conc2) - #print(Δn) + # print(Δn) G, T, c1, c2, t, conv, σ, kTST = [self.data[key] for key in ["G", "T", "c1", "c2", "t", "conv", "σ", 'kTST']] @@ -266,14 +260,14 @@ def conv_from_kinetics(kTST, time, conc1, conc2): self.set_kTST_line_edit(kTST) if kTST: - if "G" in self.is_None: #知道G不知道T + if "G" in self.is_None: # 知道G不知道T G = solve_for_ΔG(kTST, Δn, σ, T) - self.G_neq_lineEdit.setText(smart_format_float(G/1000,precision=4)) - elif "T" in self.is_None: # 知道T不知道G + self.G_neq_lineEdit.setText(smart_format_float(G / 1000, precision=4)) + elif "T" in self.is_None: # 知道T不知道G T = solve_for_T(kTST, Δn, σ, G) - self.temp_lineEdit.setText(smart_format_float(T-273.15,precision=2,scientific_notation_limit=6)) + self.temp_lineEdit.setText(smart_format_float(T - 273.15, precision=2, scientific_notation_limit=6)) - #不知道动力学,从kTST算时间、转化率 + # 不知道动力学,从kTST算时间、转化率 if "G" not in self.is_None and "T" not in self.is_None: print(f"Calculating rate constant from TST.\n Δn: {Δn}, σ: {σ}, T: {T} K, ΔG: {G} J/mol.") kTST = get_k_TST(Δn, σ, T, G) @@ -283,18 +277,16 @@ def conv_from_kinetics(kTST, time, conc1, conc2): self.total_time_lineEdit.setText(smart_format_float(t)) elif 'conv' in self.is_None: conv = conv_from_kinetics(kTST, t, c1, c2) - self.conversion_lineEdit.setText(smart_format_float(conv*100)) + self.conversion_lineEdit.setText(smart_format_float(conv * 100)) print("---------------------------------\n\n") - if __name__ == '__main__': my_Qt_Program = myWidget() my_Qt_Program.show() sys.exit(Application.exec_()) - """ Verification cases: diff --git a/Eyring_Eq.spec b/Eyring_Eq.spec new file mode 100644 index 0000000..d0647d0 --- /dev/null +++ b/Eyring_Eq.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['Eyring_Eq.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='Eyring_Eq', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['UI\\Eyring_Eq.ico'], +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='Eyring_Eq', +) diff --git a/Python_Lib/My_Lib_PyQt.py b/Python_Lib/My_Lib_PyQt.py index f60a87c..1f5299e 100644 --- a/Python_Lib/My_Lib_PyQt.py +++ b/Python_Lib/My_Lib_PyQt.py @@ -46,7 +46,6 @@ def set_Windows_scaling_factor_env_var(): import platform if platform.system() == 'Windows': import ctypes - try: import win32api MDT_EFFECTIVE_DPI = 0 @@ -60,6 +59,7 @@ def set_Windows_scaling_factor_env_var(): DPI_ratio_for_device = ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100 PyQt_scaling_ratio = QApplication.primaryScreen().devicePixelRatio() print(f"Windows 10 High-DPI debug:",end=' ') + Windows_DPI_ratio = DPI_ratio_for_monitor if DPI_ratio_for_monitor else DPI_ratio_for_device if DPI_ratio_for_monitor: print("Using monitor DPI.") ratio_of_ratio = DPI_ratio_for_monitor / PyQt_scaling_ratio @@ -73,6 +73,28 @@ def set_Windows_scaling_factor_env_var(): print(f"Using GUI high-DPI ratio: {use_ratio}") print("----------------------------------------------------------------------------") os.environ["QT_SCALE_FACTOR"] = use_ratio + else: + print("Ratio of ratio near 1. Not scaling.") + + return Windows_DPI_ratio,PyQt_scaling_ratio + + +def get_matplotlib_DPI_setting(Windows_DPI_ratio): + matplotlib_DPI_setting = 60 + if platform.system() == 'Windows': + matplotlib_DPI_setting = 60/Windows_DPI_ratio + if os.path.isfile("__matplotlib_DPI_Manual_Setting.txt"): + matplotlib_DPI_manual_setting = open("__matplotlib_DPI_Manual_Setting.txt").read() + if is_int(matplotlib_DPI_manual_setting): + matplotlib_DPI_setting = matplotlib_DPI_manual_setting + else: + with open("__matplotlib_DPI_Manual_Setting.txt",'w') as matplotlib_DPI_Manual_Setting_file: + matplotlib_DPI_Manual_Setting_file.write("") + matplotlib_DPI_setting = int(matplotlib_DPI_setting) + print(f"\nMatplotlib DPI: {matplotlib_DPI_setting}. \nSet an appropriate integer in __matplotlib_DPI_Manual_Setting.txt if the preview size doesn't match the output.\n") + + return matplotlib_DPI_setting + def get_open_directories(): if not Qt.QApplication.instance(): diff --git a/pyinstaller.py b/pyinstaller.py new file mode 100644 index 0000000..7e3dff8 --- /dev/null +++ b/pyinstaller.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +__author__ = 'LiYuanhe' + +import sys +import os +import math +import copy +import shutil +import re +import time +import random +import subprocess +from collections import OrderedDict + +# import pathlib +# parent_path = str(pathlib.Path(__file__).parent.resolve()) +# sys.path.insert(0,parent_path) + +from Python_Lib.My_Lib_Stock import * + +import PyInstaller.__main__ + +main_py_file = 'Eyring_Eq.py' +generated_exe_name = "__Eyring Eq 1.1.exe" +icon = r"UI\Eyring_Eq.ico" +include_all_folder_contents = [] +include_folders = ["UI", "Python_Lib",r"C:\Anaconda3\Lib\site-packages\setuptools"] +include_files = [] +delete_files = ["Qt5WebEngineCore.dll", + "mkl_avx512.1.dll", + "mkl_avx.1.dll", + "mkl_mc3.1.dll", + "mkl_avx2.1.dll", + "mkl_mc.1.dll", + "mkl_tbb_thread.1.dll", + "mkl_sequential.1.dll", + "mkl_vml_avx.1.dll", + "mkl_vml_mc.1.dll", + "mkl_vml_avx2.1.dll", + "mkl_vml_mc3.1.dll", + "mkl_vml_mc2.1.dll", + "mkl_vml_avx512.1.dll", + "mkl_vml_def.1.dll", + "mkl_vml_cmpt.1.dll"] + + +PyInstaller.__main__.run([ + main_py_file, + "--icon",icon, '-y' +]) + + +def copy_folder(src, dst): + """ + + :param src: + :param dst: dst will *contain* src folder + :return: + """ + target = os.path.realpath(os.path.join(dst, filename_class(src).name)) + if os.path.isdir(target): + # input('Confirm delete: '+target+" >>>") + try: + shutil.rmtree(target) + print("Deleting:", target) + except Exception: + print("Delete Failed:", target) + return None + print("Copying:", src, 'to', dst) + shutil.copytree(src, target) + + +generated_folder_name = os.path.join('dist',filename_class(main_py_file).name_stem) + + +for file in include_files: + print(f"Copying {file} to {generated_folder_name}") + shutil.copy(file,generated_folder_name) + +for folder in include_folders: + copy_folder(folder, generated_folder_name) + +for file in delete_files: + file = os.path.join(generated_folder_name,file) + if os.path.isfile(file): + print(f"Deleting {file}") + os.remove(file) + else: + print(f"File to remove not exist: {file}") + +for folder in include_all_folder_contents: + target = os.path.realpath(os.path.join(generated_folder_name, filename_class(folder).name)) + for current_object in os.listdir(folder): + current_object = os.path.join(folder, current_object) + if os.path.isfile(current_object): + shutil.copy(current_object, generated_folder_name) + else: + copy_folder(current_object, generated_folder_name) + +shutil.move(os.path.join(generated_folder_name,filename_class(main_py_file).name_stem+'.exe'), + os.path.join(generated_folder_name,generated_exe_name)) + +open_explorer_and_select(os.path.realpath(generated_folder_name)) \ No newline at end of file