Coverage for graphica/__main__.py: 27.4%

72 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1import sys 

2import os 

3import logging 

4import faulthandler 

5 

6# Win32 の SetProcessDpiAwareness() を呼ばないこと。DPI の認識モードはプロセスで1回しか設定できず、 

7# 先に呼ぶと Qt の設定が失敗して、描画とクリックの位置がずれる。 

8 

9from PySide6.QtCore import Qt, QTimer 

10from PySide6.QtWidgets import QApplication 

11from PySide6.QtGui import QFont, QGuiApplication 

12 

13# ここでは軽いモジュールだけを読む。pandas・scipy・matplotlib を引くもの(main_app_window・plugin_api)は 

14# 起動画面を出してから main() の中で読む(読み込みに 2〜3 秒かかり、その間に何も出ないと起動に気づけない) 

15from graphica.gui import app_settings 

16from graphica.gui.crash_handler import install_crash_handler, prompt_safe_mode_and_apply 

17from graphica.core.i18n import set_language, tr 

18from graphica.core.version import LOG_FILE_NAME 

19from graphica.core.app_paths import get_app_data_dir 

20 

21# UI のフォント。無ければ Qt が次の候補を使う 

22APP_FONT_FAMILIES = ["Yu Gothic UI", "Meiryo UI", "Segoe UI"] 

23APP_FONT_POINT_SIZE = 9.5 

24 

25# faulthandler が書き込む先。閉じられると書けなくなるので、プロセスの間ずっと持っておく 

26_fault_log_file = None 

27 

28 

29def _setup_logging(): 

30 """ログはファイルにも書く(exe ではコンソールが無い)。場所は get_app_data_dir()(Program Files の下には書けない)。""" 

31 log_path = os.path.join(get_app_data_dir(), LOG_FILE_NAME) 

32 _enable_native_crash_log(log_path) 

33 handlers = [logging.FileHandler(log_path, encoding="utf-8")] 

34 # コンソールの無い起動(pip の gui-scripts・exe)では sys.stdout が None 

35 if sys.stdout is not None: 

36 handlers.append(logging.StreamHandler(sys.stdout)) 

37 logging.basicConfig( 

38 level=logging.INFO, 

39 format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", 

40 handlers=handlers, 

41 ) 

42 # グラフのフォントは OS ごとの候補を並べていて、無いものごとに描くたび警告が出てログが膨らむ 

43 logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR) 

44 

45def _enable_native_crash_log(log_path): 

46 """Qt の中の不正アクセスのようなネイティブなクラッシュは Python の例外にならず、crash_handler にも届かないので 

47 何も残らない。そのときの Python のスタックを同じログに追記させる(利用者に送ってもらうファイルを1つにするため)。""" 

48 global _fault_log_file 

49 try: 

50 _fault_log_file = open(log_path, "a", encoding="utf-8") 

51 faulthandler.enable(_fault_log_file) 

52 except (OSError, ValueError): 

53 logging.getLogger(__name__).warning("ネイティブなクラッシュの記録を有効にできませんでした", exc_info=True) 

54 

55 

56def _safe_mode_flag_requested(argv): 

57 """--safe-mode が指定されたか(ウィンドウを作らずにテストできるよう分けてある)。""" 

58 return "--safe-mode" in argv 

59 

60 

61def main(): 

62 _setup_logging() 

63 # 拡大率の違うモニターの間で、描画とクリックの位置がずれないよう、倍率を丸めない。QApplication より前に 

64 QGuiApplication.setHighDpiScaleFactorRoundingPolicy( 

65 Qt.HighDpiScaleFactorRoundingPolicy.PassThrough 

66 ) 

67 install_crash_handler() 

68 app = QApplication(sys.argv) 

69 

70 app_font = QFont() 

71 app_font.setFamilies(APP_FONT_FAMILIES) 

72 app_font.setPointSizeF(APP_FONT_POINT_SIZE) 

73 app.setFont(app_font) 

74 

75 # ファイルを開いて起動されたとき、すでに起動中の Graphica があればそちらの新しいタブで開いて、こちらは終わる 

76 from graphica.gui.single_instance import (FileOpenEventFilter, InstanceServer, files_to_open, instance_running, 

77 send_to_running_instance) 

78 launch_files = files_to_open(sys.argv) 

79 if launch_files and send_to_running_instance(launch_files): 

80 return 

81 

82 settings = app_settings.open_settings() 

83 # 起動画面の文字の言語。最初のタブも同じ値で設定し直す 

84 set_language(app_settings.LANGUAGE.read(settings)) 

85 

86 # プラグインは最初のタブを作るときに読むので、その前に決める。尋ねる画面は起動画面より前に出す(後ろに隠れないように) 

87 safe_mode_requested = _safe_mode_flag_requested(sys.argv) 

88 if not safe_mode_requested: 

89 # 前回が異常終了なら、プラグインなしで起動するか尋ねる。clean_exit を書き換えるのは PlotterApp だけ 

90 prompt_safe_mode_and_apply(settings) 

91 

92 from graphica.gui.splash import show_startup_splash 

93 splash = show_startup_splash(dark=app_settings.DARK_MODE.read(settings)) 

94 

95 splash.show_progress(tr("ライブラリを読み込み中…")) 

96 from graphica.core.plugin_api import set_safe_mode 

97 from graphica.gui.main_app_window import MainAppWindow 

98 from graphica.gui.theme import disable_scroll_value_change 

99 

100 disable_scroll_value_change() 

101 if safe_mode_requested: 

102 # 明示的に指定されたので尋ねない 

103 set_safe_mode(True) 

104 

105 splash.show_progress(tr("ウィンドウを準備中…")) 

106 window = MainAppWindow() 

107 window.show() 

108 # 復元の確認やようこそ画面は、イベントループが回ってから出るので、その前に閉じる 

109 splash.finish(window) 

110 

111 # あとから起動したアプリが送ってきたファイルは新しいタブで開く。ほかの Graphica がすでに受け付けていれば譲る 

112 instance_server = InstanceServer(parent=window) 

113 if not instance_running(): 

114 instance_server.listen() 

115 instance_server.open_requested.connect(window.open_files) 

116 # macOS の Finder から開いたファイル(起動のきっかけになったものも、起動中のものも) 

117 file_open_filter = FileOpenEventFilter(window) 

118 file_open_filter.open_requested.connect(lambda paths: window.open_files(paths, reuse_empty_tab=True)) 

119 app.installEventFilter(file_open_filter) 

120 if launch_files: 

121 # 復元の確認などの後に開く(どちらもイベントループが回ってから順に動く) 

122 QTimer.singleShot(0, lambda: window.open_files(launch_files, reuse_empty_tab=True)) 

123 

124 sys.exit(app.exec()) 

125 

126 

127if __name__ == '__main__': 

128 main()