個人ビジネスをやっていると、保険などいろいろなお知らせが、電子申請サービスから送られてきます。
zipで複数のファイルがひとまとめにされていて、メインはxmlファイルなのだが、これを見るのがいろいろと面倒なのです。
という訳で、e-Govファイルビューワを作ってみました。
特徴
・eGovからdownloadしたzipファイルのまま、閲覧・PDF化・印刷が可能
・複数xmlを含む場合は選択して閲覧・PDF化・印刷が可能
簡易的なものなので、ごくたまにレイアウトが崩れたりします。
また、私が見たことのない文書は、大きく崩れる可能性があります。
中で悪いことをやっていないことをご確認ください
Python
"""
XML/XSLT Viewer & PDF Converter
ZIP input support with Windows file-open compatibility.
©2026 Sketlab LLC.
"""
import os
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
# 画面が真っ白・真っ黒になるのを防ぐ
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-gpu-compositing --no-sandbox"
from lxml import etree
from PySide6.QtCore import QMarginsF, QRect, QSize, Qt, QTimer, QUrl
from PySide6.QtGui import QFont, QIcon, QPageLayout, QPageSize, QPainter
from PySide6.QtPdf import QPdfDocument
from PySide6.QtPrintSupport import QPrintDialog, QPrinter
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QFrame,
QHBoxLayout,
QLabel,
QListWidget,
QMainWindow,
QMessageBox,
QPushButton,
QSizePolicy,
QTextEdit,
QVBoxLayout,
QWidget,
)
def get_folder_path():
"""プログラムが動いているフォルダの場所を返す"""
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
def get_icon_file():
"""アイコンのファイルの場所を探す"""
base_folder = get_folder_path()
check_paths = [
os.path.join(base_folder, "assets", "xml_viewer_icon.ico"),
os.path.join(base_folder, "xml_viewer_icon.ico"),
]
for file_path in check_paths:
if os.path.exists(file_path):
return file_path
return None
class FileSentakuDialog(QDialog):
"""ZIPの中にXMLが何個もあるときに選ぶ画面"""
def __init__(self, xml_list, parent=None):
super().__init__(parent)
self.setWindowTitle("XMLファイル選択")
self.resize(700, 400)
self.erandata_file = None
layout = QVBoxLayout(self)
setsumei_label = QLabel("ZIPファイルの中にXMLファイルが複数あります。どれを開くか選んでください。")
layout.addWidget(setsumei_label)
self.list_widget = QListWidget()
self.list_widget.addItems(xml_list)
self.list_widget.itemDoubleClicked.connect(self.ok_oshita)
layout.addWidget(self.list_widget)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.ok_oshita)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def ok_oshita(self):
item = self.list_widget.currentItem()
if item:
self.erandata_file = item.text()
self.accept()
return
QMessageBox.warning(self, "未選択", "XMLファイルを選んでください。")
class MainApp(QMainWindow):
"""メインの画面"""
def __init__(self):
super().__init__()
# タイトルバーに著作権表記を追加
self.setWindowTitle("XML/XSLT Viewer & PDF Converter - ©2026 Sketlab LLC.")
self.resize(1000, 900)
self.gamen_no_mannaka_ni_ido()
self.setAcceptDrops(True)
self.xml_path = None
self.xsl_path = None
self.pdf_hozon_path = None
self.temp_folder = None
self.zip_path = None
self.page_size_id = QPageSize.PageSizeId.A4
self.page_muki = QPageLayout.Orientation.Portrait
self.page_haba_mm = 210
self.page_tasa_mm = 297
self.page_bairitsu = 1.0
self.content_haba_px = 0
self.content_tasa_px = 0
self.scaled_haba_px = 0
self.scaled_tasa_px = 0
self.use_scaled_layout = False
self.page_yohaku_mm = 5
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(5)
ue_bar = QHBoxLayout()
self.open_button = QPushButton("開く")
self.open_button.setFixedSize(80, 30)
self.open_button.clicked.connect(self.file_open_dialog)
ue_bar.addWidget(self.open_button)
self.hozon_button = QPushButton("PDFに保存")
self.hozon_button.setFixedSize(120, 30)
self.hozon_button.clicked.connect(self.pdf_hozon_suru)
self.hozon_button.setEnabled(False)
ue_bar.addWidget(self.hozon_button)
self.print_button = QPushButton("印刷")
self.print_button.setFixedSize(80, 30)
self.print_button.clicked.connect(self.insatsu_suru)
self.print_button.setEnabled(False)
ue_bar.addWidget(self.print_button)
ue_bar.addStretch()
self.license_button = QPushButton("ライセンス")
self.license_button.setCursor(Qt.PointingHandCursor)
self.license_button.setStyleSheet(
"""
QPushButton {
border: none;
color: #555;
background: transparent;
text-decoration: underline;
font-size: 12px;
}
QPushButton:hover { color: blue; }
"""
)
self.license_button.clicked.connect(self.show_license_gamen)
ue_bar.addWidget(self.license_button)
main_layout.addLayout(ue_bar)
info_box = QWidget()
info_box.setFixedHeight(80)
info_layout = QVBoxLayout(info_box)
info_layout.setContentsMargins(0, 5, 0, 5)
info_layout.setSpacing(5)
row1 = QHBoxLayout()
xml_title_label = QLabel("XML FILE")
xml_title_label.setFont(QFont("Arial", 9, QFont.Bold))
row1.addWidget(xml_title_label)
self.xml_label = QLabel("")
self.xml_label.setFixedHeight(30)
self.xml_label.setStyleSheet("border: 1px solid #333; padding-left: 5px; background: white; color: #333;")
self.xml_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
row1.addWidget(self.xml_label, stretch=1)
row1.addSpacing(20)
xsl_title_label = QLabel("XSL FILE")
xsl_title_label.setFont(QFont("Arial", 9, QFont.Bold))
row1.addWidget(xsl_title_label)
self.xsl_label = QLabel("")
self.xsl_label.setFixedHeight(30)
self.xsl_label.setStyleSheet("border: 1px solid #333; padding-left: 5px; background: white; color: #333;")
self.xsl_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
row1.addWidget(self.xsl_label, stretch=1)
info_layout.addLayout(row1)
row2 = QHBoxLayout()
zip_title_label = QLabel("ZIP FILE")
zip_title_label.setFont(QFont("Arial", 9, QFont.Bold))
row2.addWidget(zip_title_label)
self.zip_label = QLabel("")
self.zip_label.setFixedHeight(30)
self.zip_label.setStyleSheet("border: 1px solid #333; padding-left: 5px; background: white; color: #333;")
self.zip_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
row2.addWidget(self.zip_label, stretch=1)
info_layout.addLayout(row2)
main_layout.addWidget(info_box)
sen = QFrame()
sen.setFrameShape(QFrame.HLine)
sen.setFrameShadow(QFrame.Plain)
sen.setStyleSheet("color: #005580; background-color: #005580; min-height: 2px;")
main_layout.addWidget(sen)
self.web_gamen = QWebEngineView()
self.web_gamen.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.web_gamen.setStyleSheet("background-color: #525659;")
main_layout.addWidget(self.web_gamen)
QTimer.singleShot(100, self.hikisuu_check)
def closeEvent(self, event):
self.kesu_temp_folder()
super().closeEvent(event)
def kesu_temp_folder(self):
if self.temp_folder and os.path.exists(self.temp_folder):
try:
shutil.rmtree(self.temp_folder)
except Exception:
pass
self.temp_folder = None
def gamen_no_mannaka_ni_ido(self):
try:
screen_size = self.screen().availableGeometry()
x = (screen_size.width() - self.width()) // 2 + screen_size.x()
y = (screen_size.height() - self.height()) // 2 + screen_size.y()
self.move(x, y)
except Exception:
pass
def show_license_gamen(self):
license_files = ["LICENSE", "LICENSE.txt", "license.txt"]
content = "©2026 Sketlab LLC.\n\nLicense file not found."
base_folder = os.path.dirname(os.path.abspath(sys.argv[0]))
for file_name in license_files:
file_path = os.path.join(base_folder, file_name)
if not os.path.exists(file_path):
continue
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
break
except Exception as error:
content = f"Error reading license file:\n{error}"
dialog = QDialog(self)
dialog.setWindowTitle("License")
dialog.resize(600, 400)
layout = QVBoxLayout(dialog)
text_edit = QTextEdit()
text_edit.setReadOnly(True)
text_edit.setPlainText(content)
text_edit.setFont(QFont("Consolas", 10))
layout.addWidget(text_edit)
close_button = QPushButton("閉じる")
close_button.clicked.connect(dialog.close)
layout.addWidget(close_button)
dialog.exec()
def hikisuu_check(self):
args = sys.argv[1:]
if not args:
return
input_path = args[0]
if os.path.isfile(input_path):
self.file_wo_hiraite_shori(input_path)
if len(args) > 1:
arg2 = args[1]
if arg2.lower().endswith(".xsl"):
self.xsl_yomikomi(arg2)
if len(args) > 2:
self.pdf_hozon_path = args[2]
else:
self.pdf_hozon_path = arg2
def file_open_dialog(self):
path, _ = QFileDialog.getOpenFileName(
self,
"XML/ZIPファイルを開く",
"",
"XML or ZIP Files (*.xml *.zip);;XML Files (*.xml);;ZIP Files (*.zip);;All Files (*.*)",
)
if path:
self.file_wo_hiraite_shori(path)
def file_wo_hiraite_shori(self, path):
kakuchago = os.path.splitext(path)[1].lower()
if kakuchago == ".zip":
self.zip_yomikomi(path)
return
if kakuchago == ".xml":
self.zip_path = None
self.zip_label.setText("")
self.zip_label.setToolTip("")
self.xml_yomikomi(path)
return
if kakuchago == ".xsl":
self.xsl_yomikomi(path)
return
QMessageBox.warning(self, "非対応", "XML / ZIP / XSL ファイルを指定してください。")
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.accept()
return
event.ignore()
def dropEvent(self, event):
files = [url.toLocalFile() for url in event.mimeData().urls()]
if not files:
return
first_file = files[0]
self.file_wo_hiraite_shori(first_file)
for path in files[1:]:
if path.lower().endswith(".xsl"):
self.xsl_yomikomi(path)
break
def zip_yomikomi(self, zip_file_path):
try:
self.kesu_temp_folder()
self.temp_folder = tempfile.mkdtemp(prefix="egov_xml_")
self.zip_path = zip_file_path
with zipfile.ZipFile(zip_file_path, "r") as zip_file:
self.zip_unzip_anzen(zip_file, self.temp_folder)
self.zip_label.setText(os.path.basename(zip_file_path))
self.zip_label.setToolTip(zip_file_path)
xml_files = self.folder_no_naka_sagasu(self.temp_folder, [".xml"])
if not xml_files:
QMessageBox.warning(self, "エラー", "ZIPファイルの中にXMLファイルがありませんでした。")
return
xml_files = self.xml_narabetikae(xml_files)
rel_xml_files = [os.path.relpath(path, self.temp_folder) for path in xml_files]
if len(xml_files) == 1:
selected_xml = xml_files[0]
else:
dialog = FileSentakuDialog(rel_xml_files, self)
if dialog.exec() != QDialog.Accepted or not dialog.erandata_file:
return
selected_xml = os.path.join(self.temp_folder, dialog.erandata_file)
self.xml_yomikomi(selected_xml)
except Exception as error:
QMessageBox.critical(self, "エラー", f"ZIPファイルの読み込みに失敗しました:\n{error}")
def zip_unzip_anzen(self, zip_file, nani_folder):
base_dir = Path(nani_folder).resolve()
for member in zip_file.infolist():
member_name = member.filename
if not member_name or member_name.endswith("/"):
continue
target_path = (base_dir / member_name).resolve()
if os.path.commonpath([str(base_dir), str(target_path)]) != str(base_dir):
raise ValueError(f"ZIP内に不正なパスが含まれています: {member_name}")
target_path.parent.mkdir(parents=True, exist_ok=True)
with zip_file.open(member, "r") as source, open(target_path, "wb") as target:
shutil.copyfileobj(source, target)
def xml_narabetikae(self, xml_files):
def score_keisan(path):
name = os.path.basename(path).lower()
stem = os.path.splitext(name)[0]
total = 0
if re.fullmatch(r"\d{8,}", stem):
total += 50
if "yoshiki" in name:
total += 10
if "kagami" in name:
total -= 20
if "manifest" in name:
total -= 10
try:
total += min(os.path.getsize(path) // 1024, 100)
except Exception:
pass
return total
return sorted(xml_files, key=score_keisan, reverse=True)
def folder_no_naka_sagasu(self, root_folder, kakuchago_list):
found_files = []
target_kakuchago = {k.lower() for k in kakuchago_list}
for root, _, files in os.walk(root_folder):
for file_name in files:
_, ext = os.path.splitext(file_name)
if ext.lower() in target_kakuchago:
found_files.append(os.path.join(root, file_name))
return found_files
def xml_yomikomi(self, path):
self.xml_path = path
self.xml_label.setText(os.path.basename(path))
self.xml_label.setToolTip(path)
self.xsl_path = None
self.xsl_label.setText("(検索中...)")
self.xsl_label.setToolTip("")
found_xsl = self.xml_ni_au_xsl_sagasu(path)
if found_xsl and os.path.exists(found_xsl):
self.xsl_yomikomi(found_xsl)
return
self.xsl_label.setText("(見つかりません)")
self.web_gamen.setHtml("")
self.hozon_button.setEnabled(False)
self.print_button.setEnabled(False)
QMessageBox.warning(
self,
"XSL未検出",
"XMLに対応するXSLファイルが見つかりませんでした。\n必要であればXSLファイルを別途ドラッグ&ドロップしてください。",
)
def xml_ni_au_xsl_sagasu(self, xml_file_path):
found = self.xml_no_naka_kara_xsl_sagasu(xml_file_path)
if found and os.path.exists(found):
return found
base_xsl = os.path.splitext(xml_file_path)[0] + ".xsl"
if os.path.exists(base_xsl):
return base_xsl
xml_dir = os.path.dirname(xml_file_path)
local_xsls = self.folder_no_naka_sagasu(xml_dir, [".xsl"])
if len(local_xsls) == 1:
return local_xsls[0]
if len(local_xsls) > 1:
best = self.ichiban_ii_xsl_erabu(xml_file_path, local_xsls)
if best:
return best
if self.temp_folder and os.path.exists(self.temp_folder):
all_xsls = self.folder_no_naka_sagasu(self.temp_folder, [".xsl"])
if len(all_xsls) == 1:
return all_xsls[0]
if len(all_xsls) > 1:
best = self.ichiban_ii_xsl_erabu(xml_file_path, all_xsls)
if best:
return best
return None
def xml_no_naka_kara_xsl_sagasu(self, xml_file_path):
try:
with open(xml_file_path, "rb") as f:
head = f.read(4096)
match = re.search(br'<\?xml-stylesheet[^>]*href=["\']([^"\']+)["\']', head, flags=re.IGNORECASE)
if match:
xsl_name = match.group(1).decode("utf-8", errors="ignore")
candidate = os.path.normpath(os.path.join(os.path.dirname(xml_file_path), xsl_name))
if os.path.exists(candidate):
return candidate
except Exception as error:
print(f"PI read error: {error}")
try:
tree = etree.parse(xml_file_path)
pis = tree.xpath("//processing-instruction('xml-stylesheet')")
for pi in pis:
match = re.search(r'href=["\'](.*?)["\']', pi.text)
if not match:
continue
xsl_name = match.group(1)
candidate = os.path.normpath(os.path.join(os.path.dirname(xml_file_path), xsl_name))
if os.path.exists(candidate):
return candidate
except Exception as error:
print(f"Error parsing XML for PI: {error}")
return None
def ichiban_ii_xsl_erabu(self, xml_file_path, xsl_list):
xml_name = os.path.splitext(os.path.basename(xml_file_path))[0].lower()
def score_keisan(xsl_path):
name = os.path.basename(xsl_path).lower()
total = 0
if xml_name in name:
total += 50
if "kagami" in name:
total += 20
if "yoshiki" in name:
total += 20
return total
sorted_list = sorted(xsl_list, key=score_keisan, reverse=True)
return sorted_list[0] if sorted_list else None
def xsl_yomikomi(self, path):
self.xsl_path = path
self.xsl_label.setText(os.path.basename(path))
self.xsl_label.setToolTip(path)
self.html_ni_henkan_suru()
def check_html_size(self, html_str):
width_values = [int(val) for val in re.findall(r'width\s*[:=]\s*["\']?(\d+)\s*px', html_str, flags=re.IGNORECASE)]
height_values = [int(val) for val in re.findall(r'height\s*[:=]\s*["\']?(\d+)\s*px', html_str, flags=re.IGNORECASE)]
max_width_px = max(width_values, default=640)
max_height_px = max(height_values, default=940)
use_scaled_layout = bool(width_values or height_values)
if use_scaled_layout:
max_width_px += 24
max_height_px += 12
portrait_inner_width_px = (210 - 16) * 96 / 25.4
portrait_inner_height_px = (297 - 16) * 96 / 25.4
landscape_inner_width_px = (297 - 16) * 96 / 25.4
landscape_inner_height_px = (210 - 16) * 96 / 25.4
portrait_scale = min(
portrait_inner_width_px / max_width_px,
portrait_inner_height_px / max_height_px,
)
landscape_scale = min(
landscape_inner_width_px / max_width_px,
landscape_inner_height_px / max_height_px,
)
safety_scale = 0.992
if landscape_scale > portrait_scale:
orientation = QPageLayout.Orientation.Landscape
page_width_mm = 297
page_height_mm = 210
scale = landscape_scale
else:
orientation = QPageLayout.Orientation.Portrait
page_width_mm = 210
page_height_mm = 297
scale = portrait_scale
scale = min(1.0, scale * safety_scale) if scale > 0 else 1.0
if not use_scaled_layout:
scale = 1.0
scaled_width_px = max_width_px
scaled_height_px = max_height_px
else:
scaled_width_px = max(1, int(max_width_px * scale))
scaled_height_px = max(1, int(max_height_px * scale))
page_margin_mm = 0 if use_scaled_layout else 5
return (
QPageSize.PageSizeId.A4,
orientation,
page_width_mm,
page_height_mm,
scale,
max_width_px,
max_height_px,
scaled_width_px,
scaled_height_px,
use_scaled_layout,
page_margin_mm,
)
def page_layout_tsukuru(self):
return QPageLayout(
QPageSize(self.page_size_id),
self.page_muki,
QMarginsF(self.page_yohaku_mm, self.page_yohaku_mm, self.page_yohaku_mm, self.page_yohaku_mm),
QPageLayout.Unit.Millimeter,
)
def html_ni_henkan_suru(self):
if not self.xml_path or not self.xsl_path:
return
try:
xml_doc = etree.parse(self.xml_path)
xsl_doc = etree.parse(self.xsl_path)
transform = etree.XSLT(xsl_doc)
result_tree = transform(xml_doc)
html_str = str(result_tree)
(
self.page_size_id,
self.page_muki,
self.page_haba_mm,
self.page_tasa_mm,
self.page_bairitsu,
self.content_haba_px,
self.content_tasa_px,
self.scaled_haba_px,
self.scaled_tasa_px,
self.use_scaled_layout,
self.page_yohaku_mm,
) = self.check_html_size(html_str)
a4_style = """
<style>
html {
background-color: #525659;
height: 100%;
}
body {
width: __PAGE_WIDTH__mm;
min-height: __PAGE_HEIGHT__mm;
margin: 20px auto;
padding: __SCREEN_PADDING__;
background-color: white;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
box-sizing: border-box;
font-family: "Yu Gothic", "Meiryo", sans-serif;
overflow: __BODY_OVERFLOW__;
}
p, div, span, font, td, th, li, dt, dd, a, nobr {
word-break: normal !important;
overflow-wrap: break-word !important;
}
pre {
white-space: pre-wrap !important;
word-break: break-all !important;
overflow-wrap: break-word !important;
font-family: inherit !important;
}
img {
max-width: 100% !important;
height: auto !important;
}
table.outline {
border: none !important;
}
.page-scale-host {
width: __HOST_WIDTH_PX__px;
height: __HOST_HEIGHT_PX__px;
overflow: hidden;
margin: 0 auto;
}
.page-scale-inner {
width: __CONTENT_WIDTH_PX__px;
transform: scale(__PAGE_SCALE__);
transform-origin: top left;
}
@media print {
@page {
size: __PRINT_PAGE_SIZE__;
margin: 0;
}
html {
background: white !important;
height: auto;
}
body {
width: __PAGE_WIDTH__mm;
margin: 0;
padding: __PRINT_PADDING__;
min-height: 0;
box-shadow: none;
overflow: __BODY_OVERFLOW__;
background: white;
}
.page-scale-host {
width: __PRINT_HOST_WIDTH_PX__px;
height: __PRINT_HOST_HEIGHT_PX__px;
overflow: hidden;
margin: 0 auto;
}
.page-scale-inner {
transform: scale(__PAGE_SCALE__);
transform-origin: top left;
}
table { page-break-inside: auto; }
tr { page-break-inside: avoid; page-break-after: auto; }
}
</style>
"""
a4_style = a4_style.replace("__PAGE_WIDTH__", str(self.page_haba_mm))
a4_style = a4_style.replace("__PAGE_HEIGHT__", str(self.page_tasa_mm))
a4_style = a4_style.replace("__SCREEN_PADDING__", "8mm" if self.use_scaled_layout else "12mm")
a4_style = a4_style.replace("__PRINT_PADDING__", "0" if self.use_scaled_layout else "8mm")
a4_style = a4_style.replace("__BODY_OVERFLOW__", "hidden" if self.use_scaled_layout else "visible")
a4_style = a4_style.replace("__PAGE_SCALE__", f"{self.page_bairitsu:.6f}")
a4_style = a4_style.replace("__CONTENT_WIDTH_PX__", str(self.content_haba_px))
a4_style = a4_style.replace("__HOST_WIDTH_PX__", str(self.scaled_haba_px + 4))
a4_style = a4_style.replace("__HOST_HEIGHT_PX__", str(self.scaled_tasa_px + 4))
a4_style = a4_style.replace("__SCALED_WIDTH_PX__", str(self.scaled_haba_px))
a4_style = a4_style.replace("__SCALED_HEIGHT_PX__", str(self.scaled_tasa_px))
a4_style = a4_style.replace("__PRINT_HOST_WIDTH_PX__", str(self.scaled_haba_px + 8))
a4_style = a4_style.replace("__PRINT_HOST_HEIGHT_PX__", str(self.scaled_tasa_px + 8))
a4_style = a4_style.replace(
"__PRINT_PAGE_SIZE__",
"A4 landscape" if self.page_muki == QPageLayout.Orientation.Landscape else "A4 portrait",
)
if "<head>" in html_str:
html_str = html_str.replace("<head>", "<head>" + a4_style)
elif "<html>" in html_str:
html_str = html_str.replace("<html>", "<html>" + a4_style)
else:
html_str = a4_style + html_str
body_match = re.search(r"<body([^>]*)>", html_str, flags=re.IGNORECASE)
if body_match and self.use_scaled_layout:
body_open = body_match.group(0)
html_str = html_str.replace(body_open, body_open + '<div class="page-scale-host"><div class="page-scale-inner">', 1)
html_str = re.sub(r"</body>", "</div></div></body>", html_str, count=1, flags=re.IGNORECASE)
base_folder = os.path.dirname(self.xsl_path if self.xsl_path else self.xml_path)
self.web_gamen.setHtml(html_str, QUrl.fromLocalFile(base_folder + os.sep))
self.hozon_button.setEnabled(True)
self.print_button.setEnabled(True)
except Exception as error:
self.web_gamen.setHtml("")
self.hozon_button.setEnabled(False)
self.print_button.setEnabled(False)
QMessageBox.critical(self, "エラー", f"変換に失敗しました:\n{error}")
def pdf_hozon_suru(self):
default_name = ""
if self.pdf_hozon_path:
default_name = self.pdf_hozon_path
elif self.xml_path:
default_name = os.path.splitext(self.xml_path)[0] + ".pdf"
file_path, _ = QFileDialog.getSaveFileName(self, "PDF保存", default_name, "PDF Files (*.pdf)")
if not file_path:
return
layout = self.page_layout_tsukuru()
def on_pdf_finished(success):
try:
self.web_gamen.page().pdfPrintingFinished.disconnect(on_pdf_finished)
except Exception:
pass
if success:
QMessageBox.information(self, "完了", f"PDF保存完了\n{file_path}")
else:
QMessageBox.critical(self, "エラー", "PDF保存に失敗しました")
self.web_gamen.page().pdfPrintingFinished.connect(on_pdf_finished)
self.web_gamen.page().printToPdf(file_path, layout)
def insatsu_suru(self):
printer = QPrinter(QPrinter.HighResolution)
printer.setFullPage(True)
printer.setPageLayout(self.page_layout_tsukuru())
dialog = QPrintDialog(printer, self)
if dialog.exec() != QPrintDialog.Accepted:
return
printer.setFullPage(True)
printer.setPageLayout(self.page_layout_tsukuru())
fd, temp_path = tempfile.mkstemp(suffix=".pdf")
os.close(fd)
def on_pdf_ready(success):
try:
self.web_gamen.page().pdfPrintingFinished.disconnect(on_pdf_ready)
except Exception:
pass
if not success:
QMessageBox.critical(self, "エラー", "印刷用データの生成に失敗しました")
try:
os.remove(temp_path)
except Exception:
pass
return
try:
doc = QPdfDocument(self)
doc.load(temp_path)
painter = QPainter()
if painter.begin(printer):
for page_index in range(doc.pageCount()):
if page_index > 0:
printer.newPage()
layout = printer.pageLayout()
rect = layout.fullRectPixels(printer.resolution())
source_size = doc.pagePointSize(page_index).toSize()
if source_size.isEmpty():
source_size = rect.size()
target_size = QSize(source_size)
target_size.scale(rect.size(), Qt.KeepAspectRatio)
page_image = doc.render(page_index, target_size)
if page_image.isNull():
continue
x_pos = rect.x() + max(0, (rect.width() - target_size.width()) // 2)
y_pos = rect.y() + max(0, (rect.height() - target_size.height()) // 2)
target_rect = QRect(x_pos, y_pos, target_size.width(), target_size.height())
painter.drawImage(target_rect, page_image)
painter.end()
QMessageBox.information(self, "完了", "印刷が完了しました")
else:
QMessageBox.critical(self, "エラー", "プリンタの初期化に失敗しました")
except Exception as error:
QMessageBox.critical(self, "エラー", f"印刷処理中にエラーが発生しました:\n{error}")
finally:
try:
os.remove(temp_path)
except Exception:
pass
layout = self.page_layout_tsukuru()
self.web_gamen.page().pdfPrintingFinished.connect(on_pdf_ready)
self.web_gamen.page().printToPdf(temp_path, layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
# まず画面(window)を作ります
window = MainApp()
# exe化するとパスが変わるため、相対パスだと画像を見失うことがあります
if getattr(sys, 'frozen', False):
# exeから実行されている場合
base_path = sys._MEIPASS
else:
# 通常のpythonコマンドで実行されている場合
base_path = os.path.dirname(os.path.abspath(__file__))
icon_path = os.path.join(base_path, "assets", "xml_viewer_icon.ico")
if os.path.exists(icon_path):
# アプリ全体、およびウィンドウにアイコンを設定
app.setWindowIcon(QIcon(icon_path))
window.setWindowIcon(QIcon(icon_path))
window.show()
sys.exit(app.exec())レイアウトが崩れるので、見た目で成型するのに苦労しています(今でも完ぺきではない)
上記を実行ファイルにしたものを置いておきますので、ご入用の方はどうぞ。
xml_view.zip
例によってサポートは致しませんが、なにかご意見をお送りいただければ、気が向いたら修正します。
【免責事項】
本ツールは無保証で提供されます。利用による損害について開発者は一切の責任を負いません。
また、本ツールに関する個別のサポートやお問い合わせへの対応は行っておりません。
あらかじめご了承の上、自己責任でご利用ください。

