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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1import sys
2import os
3import logging
4import faulthandler
6# Win32 の SetProcessDpiAwareness() を呼ばないこと。DPI の認識モードはプロセスで1回しか設定できず、
7# 先に呼ぶと Qt の設定が失敗して、描画とクリックの位置がずれる。
9from PySide6.QtCore import Qt, QTimer
10from PySide6.QtWidgets import QApplication
11from PySide6.QtGui import QFont, QGuiApplication
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
21# UI のフォント。無ければ Qt が次の候補を使う
22APP_FONT_FAMILIES = ["Yu Gothic UI", "Meiryo UI", "Segoe UI"]
23APP_FONT_POINT_SIZE = 9.5
25# faulthandler が書き込む先。閉じられると書けなくなるので、プロセスの間ずっと持っておく
26_fault_log_file = None
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)
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)
56def _safe_mode_flag_requested(argv):
57 """--safe-mode が指定されたか(ウィンドウを作らずにテストできるよう分けてある)。"""
58 return "--safe-mode" in argv
61def main():
62 _setup_logging()
63 # 拡大率の違うモニターの間で、描画とクリックの位置がずれないよう、倍率を丸めない。QApplication より前に
64 QGuiApplication.setHighDpiScaleFactorRoundingPolicy(
65 Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
66 )
67 install_crash_handler()
68 app = QApplication(sys.argv)
70 app_font = QFont()
71 app_font.setFamilies(APP_FONT_FAMILIES)
72 app_font.setPointSizeF(APP_FONT_POINT_SIZE)
73 app.setFont(app_font)
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
82 settings = app_settings.open_settings()
83 # 起動画面の文字の言語。最初のタブも同じ値で設定し直す
84 set_language(app_settings.LANGUAGE.read(settings))
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)
92 from graphica.gui.splash import show_startup_splash
93 splash = show_startup_splash(dark=app_settings.DARK_MODE.read(settings))
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
100 disable_scroll_value_change()
101 if safe_mode_requested:
102 # 明示的に指定されたので尋ねない
103 set_safe_mode(True)
105 splash.show_progress(tr("ウィンドウを準備中…"))
106 window = MainAppWindow()
107 window.show()
108 # 復元の確認やようこそ画面は、イベントループが回ってから出るので、その前に閉じる
109 splash.finish(window)
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))
124 sys.exit(app.exec())
127if __name__ == '__main__':
128 main()