From 5c494307c136aed8dd724d38f5890842a68ffee6 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 22 Sep 2025 16:33:25 +0800
Subject: [PATCH 01/40] Add Fgui-Converter Entry

Add a Qt Widget for Fgui-Convert, which can be found in Navigator window. But "Convert" Button has no function yet.
---
 .../forms/fguiconverterwidget.ui              | 174 +++++++++++++
 .../mainwindowinterface.py                    |   5 +
 src/preppipe_gui_pyside6/navigatorwidget.py   |   2 +
 .../toolwidgets/fguiconverter.py              | 244 ++++++++++++++++++
 4 files changed, 425 insertions(+)
 create mode 100644 src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui
 create mode 100644 src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py

diff --git a/src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui b/src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui
new file mode 100644
index 0000000..e62a247
--- /dev/null
+++ b/src/preppipe_gui_pyside6/forms/fguiconverterwidget.ui
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FguiConverterWidget</class>
+ <widget class="QWidget" name="FguiConverterWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>852</width>
+    <height>406</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <widget class="QListWidget" name="listWidget">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>60</y>
+     <width>441</width>
+     <height>171</height>
+    </rect>
+   </property>
+   <property name="verticalScrollBarPolicy">
+    <enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOn</enum>
+   </property>
+   <property name="dragEnabled">
+    <bool>true</bool>
+   </property>
+   <property name="dragDropMode">
+    <enum>QAbstractItemView::DragDropMode::InternalMove</enum>
+   </property>
+   <property name="selectionBehavior">
+    <enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
+   </property>
+  </widget>
+  <widget class="QLabel" name="fguiAssetDictLabel">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>30</y>
+     <width>181</width>
+     <height>21</height>
+    </rect>
+   </property>
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="text">
+    <string>FGUI资源目录</string>
+   </property>
+  </widget>
+  <widget class="QWidget" name="horizontalLayoutWidget_2">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>300</y>
+     <width>541</width>
+     <height>41</height>
+    </rect>
+   </property>
+   <layout class="QHBoxLayout" name="horizontalLayout_2">
+    <item>
+     <widget class="QLineEdit" name="outputDictLine">
+      <property name="enabled">
+       <bool>true</bool>
+      </property>
+      <property name="text">
+       <string>(未指定)</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QPushButton" name="outputDictButton">
+      <property name="text">
+       <string>选择</string>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QPushButton" name="convertButton">
+   <property name="geometry">
+    <rect>
+     <x>610</x>
+     <y>60</y>
+     <width>121</width>
+     <height>301</height>
+    </rect>
+   </property>
+   <property name="text">
+    <string>转换</string>
+   </property>
+  </widget>
+  <widget class="QWidget" name="verticalLayoutWidget">
+   <property name="geometry">
+    <rect>
+     <x>470</x>
+     <y>60</y>
+     <width>91</width>
+     <height>181</height>
+    </rect>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <widget class="QPushButton" name="addDictButton">
+      <property name="text">
+       <string>添加</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QPushButton" name="removeDictButton">
+      <property name="text">
+       <string>删除</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QPushButton" name="openDictButton">
+      <property name="text">
+       <string>打开所在目录</string>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="Line" name="line">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>250</y>
+     <width>541</width>
+     <height>20</height>
+    </rect>
+   </property>
+   <property name="orientation">
+    <enum>Qt::Orientation::Horizontal</enum>
+   </property>
+  </widget>
+  <widget class="Line" name="line_2">
+   <property name="geometry">
+    <rect>
+     <x>570</x>
+     <y>60</y>
+     <width>20</width>
+     <height>301</height>
+    </rect>
+   </property>
+   <property name="orientation">
+    <enum>Qt::Orientation::Vertical</enum>
+   </property>
+  </widget>
+  <widget class="QLabel" name="outputDictLabel">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>270</y>
+     <width>171</width>
+     <height>39</height>
+    </rect>
+   </property>
+   <property name="text">
+    <string>Ren'Py项目基目录</string>
+   </property>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/preppipe_gui_pyside6/mainwindowinterface.py b/src/preppipe_gui_pyside6/mainwindowinterface.py
index 876caab..1efd657 100644
--- a/src/preppipe_gui_pyside6/mainwindowinterface.py
+++ b/src/preppipe_gui_pyside6/mainwindowinterface.py
@@ -61,3 +61,8 @@ def handleLanguageChange(self) -> None:
     zh_cn="WebGal 导出",
     zh_hk="WebGal 導出",
   )
+  tr_toolname_guiconverter = TR_gui_mainwindow.tr("toolname_guiconverter",
+    en="GUI Converter",
+    zh_cn="GUI 转换",
+    zh_hk="GUI 轉換",
+  )
diff --git a/src/preppipe_gui_pyside6/navigatorwidget.py b/src/preppipe_gui_pyside6/navigatorwidget.py
index 5643969..4b54913 100644
--- a/src/preppipe_gui_pyside6/navigatorwidget.py
+++ b/src/preppipe_gui_pyside6/navigatorwidget.py
@@ -12,6 +12,7 @@
 from .toolwidgets.imagepack import *
 from .toolwidgets.setting import *
 from .toolwidgets.maininput import *
+from .toolwidgets.fguiconverter import *
 
 class ToolNode:
   info : ToolWidgetInfo | None
@@ -24,6 +25,7 @@ class ToolNode:
   NAVIGATION_LIST : typing.ClassVar[list] = [
     SettingWidget,
     MainInputWidget,
+    FguiConverterWidget,
     (ImagePackWidget, {"category_kind": ImagePackDescriptor.ImagePackType.BACKGROUND}),
     (ImagePackWidget, {"category_kind": ImagePackDescriptor.ImagePackType.CHARACTER}),
   ]
diff --git a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
new file mode 100644
index 0000000..2d91e18
--- /dev/null
+++ b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
@@ -0,0 +1,244 @@
+from PySide6.QtCore import *
+from PySide6.QtGui import *
+from PySide6.QtWidgets import *
+from preppipe.language import *
+from ..forms.generated.ui_fguiconverterwidget import Ui_FguiConverterWidget
+from ..toolwidgetinterface import *
+from ..mainwindowinterface import *
+from ..settingsdict import *
+from ..translatablewidgetinterface import *
+from ..util.fileopen import FileOpenHelper
+
+TR_gui_fguiconverter = TranslationDomain("gui_fguiconverter")
+
+class FguiConverterWidget(QWidget, ToolWidgetInterface):
+  listChanged = Signal()
+  isDirectoryMode : bool
+  isExistingOnly : bool
+  verifyCB : typing.Callable[[str], bool] | None
+  fieldName : Translatable | str
+  filter : Translatable | str
+  lastAddedPath : str
+  lastOutputPath : str
+  ui : Ui_FguiConverterWidget
+
+
+  TR_gui_fguiassetsdictwidget = TranslationDomain("gui_fguiassetsdictwidget")
+  _tr_fgui_assets_dict_label = TR_gui_fguiassetsdictwidget.tr("fgui_assets_dict_label",
+      en="FairyGUI Assets Dictionary",
+      zh_cn="FairyGUI资源目录",
+      zh_hk="FairyGUI資源目錄",
+  )
+  _tr_add = TR_gui_fguiassetsdictwidget.tr("add",
+      en="Add",
+      zh_cn="添加",
+      zh_hk="添加",
+  )
+  _tr_remove = TR_gui_fguiassetsdictwidget.tr("remove",
+      en="Remove",
+      zh_cn="删除",
+      zh_hk="刪除",
+  )
+  _tr_open_directory = TR_gui_fguiassetsdictwidget.tr("open_directory",
+      en="Open Directory",
+      zh_cn="打开目录",
+      zh_hk="打開目錄",
+  )
+  _tr_output_dict_label = TR_gui_fguiassetsdictwidget.tr("output_dict_label",
+      en="Ren'Py Project Base Directory",
+      zh_cn="Ren'Py项目基目录",
+      zh_hk="Ren'Py項目基目錄",
+  )
+  _tr_output_dict_button = TR_gui_fguiassetsdictwidget.tr("output_dict_button",
+      en="Select",
+      zh_cn="选择",
+      zh_hk="選擇",
+  )
+  _tr_output_dict_line = TR_gui_fguiassetsdictwidget.tr("output_dict_line_edit",
+      en="(None)",
+      zh_cn="(未指定)",
+      zh_hk="(未指定)",
+  )
+  _tr_convert_button = TR_gui_fguiassetsdictwidget.tr("convert_button",
+      en="Convert",
+      zh_cn="转换",
+      zh_hk="轉換",
+  )
+
+  def __init__(self, parent : QWidget):
+    super(FguiConverterWidget, self).__init__(parent)
+    self.ui = Ui_FguiConverterWidget()
+    self.ui.setupUi(self)
+    self.isDirectoryMode = True
+    self.isExistingOnly = False
+    self.verifyCB = None
+    self.fieldName = ""
+    self.filter = ""
+    self.lastAddedPath = ""
+    self.lastOutputPath = ""
+
+    self.bind_text(self.ui.fguiAssetDictLabel.setText, self._tr_fgui_assets_dict_label)
+    self.bind_text(self.ui.addDictButton.setText, self._tr_add)
+    self.bind_text(self.ui.removeDictButton.setText, self._tr_remove)
+    self.bind_text(self.ui.openDictButton.setText, self._tr_open_directory)
+    self.bind_text(self.ui.outputDictLabel.setText, self._tr_output_dict_label)
+    self.bind_text(self.ui.outputDictButton.setText, self._tr_output_dict_button)
+    self.bind_text(self.ui.outputDictLine.setText, self._tr_output_dict_line)
+    self.bind_text(self.ui.convertButton.setText, self._tr_convert_button)
+
+    self.ui.listWidget.itemChanged.connect(lambda: self.listChanged.emit())
+    self.ui.addDictButton.clicked.connect(self.itemAdd)
+    self.ui.removeDictButton.clicked.connect(self.itemRemove)
+    self.ui.openDictButton.clicked.connect(self.itemOpenContainingDirectory)
+    self.setAcceptDrops(True)
+    self.ui.outputDictButton.clicked.connect(self.itemOpenOutputDirectory)
+    self.ui.outputDictLine.textChanged.connect(self.setOutputDict)
+
+  @classmethod
+  def getToolInfo(cls, **kwargs) -> ToolWidgetInfo:
+    return ToolWidgetInfo(
+      idstr="fguiconverter",
+      name="UI资源转换",
+      widget=cls,
+    )
+
+
+  def setDirectoryMode(self, v: bool):
+    self.isDirectoryMode = v
+    if self.verifyCB is None:
+      self.verifyCB = (lambda path: os.path.isdir(path)) if v else (lambda path: os.path.isfile(path))
+
+  def setVerifyCallBack(self, cb):
+    """Set a callback function that verifies a path. The function should accept a string and return a boolean."""
+    self.verifyCB = cb
+
+  def setExistingOnly(self, v: bool):
+    self.isExistingOnly = v
+
+  def setFieldName(self, name: Translatable | str):
+    self.fieldName = name
+    if isinstance(name, Translatable):
+      self.bind_text(self.ui.label.setText, name)
+    else:
+      self.ui.label.setText(name)
+
+  def setFilter(self, filter_str: Translatable | str):
+    self.filter = filter_str
+
+  def getCurrentList(self):
+    results = []
+    for i in range(self.ui.listWidget.count()):
+      item = self.ui.listWidget.item(i)
+      results.append(item.text())
+    return results
+
+  def dragEnterEvent(self, e: QDragEnterEvent):
+    if e.mimeData().hasUrls():
+      path = e.mimeData().urls()[0].toLocalFile()
+      if not self.verifyCB or self.verifyCB(path):
+        e.acceptProposedAction()
+        return
+    super().dragEnterEvent(e)
+
+  def dropEvent(self, event: QDropEvent):
+    for url in event.mimeData().urls():
+      path = url.toLocalFile()
+      if not self.verifyCB or self.verifyCB(path):
+        self.addPath(path)
+    event.acceptProposedAction()
+
+  @Slot(str)
+  def addPath(self, path: str):
+    # Prevent duplicates
+    for i in range(self.ui.listWidget.count()):
+      if self.ui.listWidget.item(i).text() == path:
+        return
+    newItem = QListWidgetItem(path)
+    newItem.setToolTip(path)
+    self.ui.listWidget.addItem(newItem)
+    self.lastAddedPath = path
+    self.listChanged.emit()
+
+  @Slot()
+  def itemMoveUp(self):
+    curRow = self.ui.listWidget.currentRow()
+    if curRow > 0:
+      item = self.ui.listWidget.takeItem(curRow)
+      self.ui.listWidget.insertItem(curRow - 1, item)
+      self.ui.listWidget.setCurrentRow(curRow - 1)
+      self.listChanged.emit()
+
+  @Slot()
+  def itemMoveDown(self):
+    curRow = self.ui.listWidget.currentRow()
+    if curRow >= 0 and curRow + 1 < self.ui.listWidget.count():
+      item = self.ui.listWidget.takeItem(curRow)
+      self.ui.listWidget.insertItem(curRow + 1, item)
+      self.ui.listWidget.setCurrentRow(curRow + 1)
+      self.listChanged.emit()
+
+  @Slot()
+  def itemOpenContainingDirectory(self):
+    curRow = self.ui.listWidget.currentRow()
+    if curRow >= 0:
+      path = self.ui.listWidget.item(curRow).text()
+      FileOpenHelper.open_containing_directory(self, path)
+
+  _tr_select_dialog_title = TR_gui_fguiassetsdictwidget.tr("select_dialog_title",
+    en="Please select FairyGUI Assets Dictionary{field}",
+    zh_cn="请选择FairyGUI资源目录{field}",
+    zh_hk="請選擇FairyGUI資源目錄{field}",
+  )
+
+  @Slot()
+  def itemAdd(self):
+    dialogTitle = self._tr_select_dialog_title.format(field=str(self.fieldName))
+    initialDir = self.lastAddedPath if self.lastAddedPath else ""
+    dialog = QFileDialog(self, dialogTitle, initialDir, str(self.filter))
+    if self.isDirectoryMode:
+      dialog.setFileMode(QFileDialog.Directory)
+      dialog.setOption(QFileDialog.ShowDirsOnly, True)
+    else:
+      dialog.setFileMode(QFileDialog.ExistingFile if self.isExistingOnly else QFileDialog.AnyFile)
+    dialog.fileSelected.connect(self.addPath)
+    dialog.show()
+
+  @Slot()
+  def itemRemove(self):
+    curRow = self.ui.listWidget.currentRow()
+    if curRow >= 0:
+      item = self.ui.listWidget.takeItem(curRow)
+      del item
+      self.listChanged.emit()
+
+  _tr_select_dialog_title_2 = TR_gui_fguiassetsdictwidget.tr("select_dialog_title_2",
+    en="Please select Ren'Py Project Base Directory{field}",
+    zh_cn="请选择Ren'Py项目基目录{field}",
+    zh_hk="請選擇Ren'Py項目基目錄{field}",
+  )
+
+  @Slot()
+  def itemOpenOutputDirectory(self):
+    dialogTitle = self._tr_select_dialog_title_2.format(field=str(self.fieldName))
+    initialDir = self.lastOutputPath if self.lastOutputPath else ""
+    dialog = QFileDialog(self, dialogTitle, initialDir, str(self.filter))
+    if self.isDirectoryMode:
+      dialog.setFileMode(QFileDialog.Directory)
+      dialog.setOption(QFileDialog.ShowDirsOnly, True)
+    else:
+      dialog.setFileMode(QFileDialog.ExistingFile if self.isExistingOnly else QFileDialog.AnyFile)
+    dialog.fileSelected.connect(self.setOutputDict)
+    dialog.show()
+
+  @Slot(str)
+  def setOutputDict(self, path: str):
+    self.ui.outputDictLine.setText(path)
+    self.lastOutputPath = path
+
+  @classmethod
+  def getToolInfo(cls, **kwargs) -> ToolWidgetInfo:
+    return ToolWidgetInfo(
+      idstr="guiconverter",
+      name=MainWindowInterface.tr_toolname_guiconverter,
+      widget=cls,
+    )

From c386335bca7bad5e8fa704e391a8bb305ce55880 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 29 Sep 2025 23:10:33 +0800
Subject: [PATCH 02/40] Add fgui_converter modul.

---
 src/fgui_converter/FguiAssetsParseLib.py      | 1017 +++++++++++++++++
 src/fgui_converter/__init__.py                |    4 +
 .../utils/renpy/Fgui2RenpyConverter.py        |  999 ++++++++++++++++
 .../renpy/renpy_templates/01_renpy_cdd.rpy    |  175 +++
 .../renpy/renpy_templates/02_renpy_shader.rpy |   99 ++
 .../renpy_ellipseAA_template.txt              |    2 +
 .../renpy_ellipse_template.txt                |    2 +
 .../renpy_font_map_definition.txt             |   14 +
 .../renpy_rectangleAA_template.txt            |    2 +
 .../renpy_rectangle_template.txt              |    2 +
 .../toolwidgets/fguiconverter.py              |   38 +-
 11 files changed, 2342 insertions(+), 12 deletions(-)
 create mode 100644 src/fgui_converter/FguiAssetsParseLib.py
 create mode 100644 src/fgui_converter/__init__.py
 create mode 100644 src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
new file mode 100644
index 0000000..a5af312
--- /dev/null
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -0,0 +1,1017 @@
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+from lxml import etree
+
+class FguiPackage():
+    """
+    FairyGUI资源包中的Package描述内容。
+    包含component列表、image列表和atlas列表。
+    """
+    desc_id = ''
+    name = ''
+    component_list = []
+    image_list = []
+    atlas_list = []
+
+    class brief_component:
+        id = ''
+        name = ''
+        path = ''
+        size = ()
+        exported = True
+        def __init__(self, id, name, path, size, exported=True):
+            self.id = id
+            self.name = name
+            self.path = path
+            size_list = size.split(",")
+            self.size = (int(size_list[0]), int(size_list[1]))
+            self.exported = exported
+
+    class brief_image:
+        id = ''
+        name = ''
+        path = ''
+        size = ()
+        scale = ''  # 九宫格：9grid；平铺：tile
+        scale9grid = []
+        exported = True
+        def __init__(self, id, name, path, size, scale=None, scale9grid=[], exported=True):
+            self.id = id
+            self.name = name
+            self.path = path
+            size_list = size.split(",")
+            self.size = (int(size_list[0]), int(size_list[1]))
+            self.scale = scale
+            if self.scale == '9grid':
+                self.scale9grid = scale9grid.split(",")
+            self.exported = exported
+
+    class brief_atlas:
+        id = ''
+        size = ()
+        file = ''
+        def __init__(self, id, size, file):
+            self.id = id
+            size_list = size.split(",")
+            self.size = (int(size_list[0]), int(size_list[1]))
+            self.file = file
+
+    def __init__(self, package_etree):
+        self.package_etree = package_etree
+        self.id = package_etree.get("id")
+        self.name = package_etree.get("name")
+        self.resource = package_etree[0]
+        self.id_name_mapping = {}
+        if (self.resource.tag != 'resources'):
+            raise ValueError('packageDescription child is not resources.')
+        for child in self.resource:
+            # component类 id, name, path, size, exported=True
+            if (child.tag == 'component'):
+                self.component_list.append(self.brief_component(
+                                                            child.get('id'),
+                                                            child.get('name'),
+                                                            child.get('path'),
+                                                            child.get('size'),
+                                                            exported=TransStrToBoolean(child.get('exported'))))
+                self.id_name_mapping[child.get('id')] = child.get('name')
+            # image类 name, path, size, scale=None, scale9grid=[], exported=True
+            if (child.tag == 'image'):
+                self.image_list.append(self.brief_image(
+                                                    child.get('id'),
+                                                    child.get('name'),
+                                                    child.get('path'),
+                                                    child.get('size'),
+                                                    child.get('scale'),
+                                                    child.get('scale9grid'),
+                                                    exported=TransStrToBoolean(child.get('exported'))))
+                self.id_name_mapping[child.get('id')] = child.get('name')
+            # atlas类 id, size, file
+            if (child.tag == 'atlas'):
+                self.atlas_list.append(self.brief_atlas(
+                                                child.get('id'),
+                                                child.get('size'),
+                                                child.get('file')))
+
+    def clear(self):
+        self.component_list.clear()
+        self.image_list.clear()
+        self.atlas_list.clear()
+        self.id_name_mapping.clear()
+
+def TransStrToBoolean(str):
+    if (str == 'true' or str == 'True'):
+        return True
+    else:
+        return False
+
+class FguiSpriteInfo:
+    """
+    FairyGUI资源包中的Sprite描述内容。
+    总计11个字段，包括image id、图集编号、在图集中的x坐标、在图集中的y坐标、image宽度、image高度、rotate、
+    原图相对image的x偏移、原图相对image的y偏移、原图宽度、原图高度。
+    """
+    def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x, offset_y, source_width, source_height):
+        self.image_id = image_id
+        self.atlas_index = int(atlas_index)
+        self.x = int(x)
+        self.y = int(y)
+        self.width = int(width)
+        self.height = int(height)
+        self.rotate = int(rotate)
+        self.offset_x = int(offset_x)
+        self.offset_y = int(offset_y)
+        self.source_width = int(source_width)
+        self.source_height = int(source_height)
+
+    def __repr__(self):
+        return f"FguiSpriteInfo({self.image_id}, {self.atlas_index}, {self.x}, {self.y}, {self.width}, {self.height}, {self.rotate}, {self.offset_x}, {self.offset_y}, {self.source_width}, {self.source_height})"
+
+# 解析纹理集描述文件。文件名通常为“项目名称@sprites.bytes”
+def ParseFguiSpriteDescFile(sprite_desc_file):
+    fgui_image_sets = []
+    with open(sprite_desc_file, "r", encoding="utf-8") as f:
+        for line in f:
+            parts = line.strip().split()
+            if len(parts) == 11:  # 确保每行有11个字段
+                obj = FguiSpriteInfo(*parts)
+                fgui_image_sets.append(obj)
+    return fgui_image_sets
+
+# 从xml字符串创建lxml的etree对象
+def GetXmlTree(xml_str):
+    root = etree.fromstring(xml_str.encode('utf-8'))
+    return root
+
+# 解析发布资源描述文件。文件名通常为“项目名称.bytes”
+def ParseFguiPackageDescFile(file_name):
+    #f = open("Package1.bytes", "r", encoding='utf-8')
+    with open(file_name, "r", encoding='utf-8') as f:
+        ori_str = f.read()
+    xml_flag_str = '.xml'
+    split_flag_str = '|'
+    xml_length = 0
+    cursor = 0
+    index = 0
+    xml_string = ''
+    xml_name = ''
+    object_dict = {}
+    index = ori_str.find(xml_flag_str)
+
+    while index != -1:
+        xml_name = ori_str[cursor:index]
+        cursor= index+len(xml_flag_str)
+        index = ori_str.find(split_flag_str, cursor, -1)
+        cursor = index + len(split_flag_str)
+        index = ori_str.find(split_flag_str, cursor, -1)
+        xml_length = int(ori_str[cursor:index])
+        cursor = index + len(split_flag_str)
+        index = cursor + xml_length
+        xml_string = ori_str[cursor: index]
+        object_dict[xml_name] = GetXmlTree(xml_string)
+        cursor = index
+        index = ori_str.find(xml_flag_str, cursor)
+
+    return object_dict
+
+
+class OriImage:
+    """
+    图像类。
+    结合两个描述文件信息的原始image类。
+    为后续生成目标引擎图像定义的基础数据结构。
+    svg生成的图片可能会带有@2x、@3x等后缀，导致图片尺寸有异，此处暂时忽略。
+    """
+    def __init__(self, image_id, name, atlas_name, atlas_pos_x, atlas_pos_y, width, height):
+        self.image_id = image_id
+        self.name = name
+        self.atlas_name = atlas_name
+        self.atlas_pos_x = atlas_pos_x
+        self.atlas_pos_y = atlas_pos_y
+        self.width = width
+        self.height = height
+
+    def __repr__(self):
+        return f"OriImage({self.image_id}, {self.name}, {self.atlas_name}, {self.atlas_pos_x}, {self.atlas_pos_y}, {self.width}, {self.height})"
+
+
+def GetOriImage(image_id, package_desc, fgui_image_sets, fgui_atlas_dicts):
+    image_set = None
+    for item in fgui_image_sets:
+        if (item.image_id == image_id):
+            image_set = item
+    atlas_name = fgui_atlas_dicts["atlas"+str(item.atlas_index)]
+    for image in package_desc.image_list:
+        if (image.id == image_id):
+            name =image.name
+    return OriImage(image_id, name, atlas_name, image_set.x, image_set.y, image_set.width, image_set.height)
+
+
+class FguiController:
+    """
+     FairyGUI控制器类
+     name：控制器名称，字符串，样例"button"
+     page：索引、索引名，字符串，样例"0,up,1,down,2,over,3,selectedOver"
+     selected：初始索引号，字符串 ，样例"0"
+    """
+    def __init__(self, name, page, selected):
+        self.name = name
+        self.page_index_dict = {}
+        page_list = page.split(',')
+        page_num = len(page_list)
+        i = 0
+        while i < page_num:
+            self.page_index_dict[int(page_list[i])] = page_list[i+1]
+            i += 2
+        self.selected = int(selected) if selected else 0
+
+    def __repr__(self):
+        return f"FguiController({self.name}, {self.page_index_dict}, {self.selected})"
+
+class FguiHitTest:
+    """
+    点击测试区域类。
+    """
+    def __init__(self, hit_test_str):
+        self.src = None
+        self.pos = (0, 0)
+        if hit_test_str:
+            src, xpos, ypos = hit_test_str.split(",")
+            self.src = src
+            self.pos = (int(xpos), int(ypos))
+
+class FguiComponent:
+    """
+    FairyGUI组件对应的基类。
+    id、name、path和size属性与package中brief_component的对应组件信息保持一致
+    """
+
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        self.component_etree = component_etree
+        self.id = id
+        self.name = name
+        self.package_desc_id = package_desc_id
+        self.size = component_etree.get("size")
+        self.extention = component_etree.get("extention")
+        self.mask = component_etree.get("mask")
+        # 轴心。默认值为(0.0, 0.0)。
+        pivot = component_etree.get("pivot", "0.0,0.0")
+        self.pivot = tuple(map(float, pivot.split(",")))
+        # 点击测试区域，包含3个字段，分别为引用源src和组件内相对坐标x、y。
+        hit_test = component_etree.get("hitTest")
+        self.hit_test = FguiHitTest(hit_test) if hit_test else None
+
+        # 控制器，一般不超过1个。
+        self.controller_list = []
+        # 显示内容列表，通常为image、text、graph。
+        self.display_list = None
+        # 一级子对象
+        self.child_num = len(self.component_etree)
+
+        for i in range(self.child_num):
+            # 控制器
+            if (self.component_etree[i].tag == "controller"):
+                self.controller_list.append(FguiController(self.component_etree[i].get("name"), self.component_etree[i].get("pages"), self.component_etree[i].get("selected")))
+            # 显示内容
+            elif (self.component_etree[i].tag == "displayList"):
+                self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id)
+
+    def __repr__(self):
+        return f"FguiComponent({self.id}, {self.name}, {self.size}, {self.extention}, {self.mask})"
+
+class FguiButton(FguiComponent):
+    """
+    FairyGUI中的按钮button。
+    相比其他component，多一个Button标签
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+        button = component_etree.find("Button")
+        self.button_mode = button.get("mode")
+        self.button_down_effect = button.get("downEffect")
+        self.button_down_effect_value = button.get("downEffectValue")
+
+class FguiScrollBar(FguiComponent):
+    """
+    FairyGUI中的滚动条scrollbar。
+    相比其他component，多一个ScrollBar标签，大部分情况为空。
+    通常滚动条都会有以个对应的其他组件，为同名带后缀“_grip”的按钮。
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+        scrollbar = component_etree.find("ScrollBar")
+
+class FguiLabel(FguiComponent):
+    """
+    FairyGUI中的标签lablel。
+    目前与FguiComponent完全相同，甚至不具有单独的Label标签。
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+
+
+class FguiComboBox(FguiComponent):
+    """
+    FairyGUI中的下拉框。
+    相比其他component，多一个ComboBox标签，属性dropdown对应点击后显示的选项列表。
+    通常下拉框都会有两个对应的其他组件，分别为同名带后缀“_item”的按钮和同名带后缀“_popup”的组件
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+        combobox = component_etree.find("ComboBox")
+        self.dropdown = combobox.get("dropdown")
+
+class FguiProgressBar(FguiComponent):
+    """
+    FairyGUI中的进度条。
+    相比其他component，多一个ProgressBar标签，大部分情况为空。
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+        combobox = component_etree.find("ProgressBar")
+
+class FguiSlider(FguiComponent):
+    """
+    FairyGUI中的滑动条。
+    相比其他component，多一个Slider标签，大部分情况为空。
+    滑动条会有一个相应的其他组件，同名带后缀“_grip”的按钮。
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+        combobox = component_etree.find("Slider")
+
+class FguiWindow(FguiComponent):
+    """
+    FairyGUI中的可拖拽窗口。
+    没有extention类型，也没有特殊标签，仅从扩展类型无法区分Label和Window。
+
+    FairyGUI中的Window子组件有一些非硬性命名约定(约束)：
+        1. 名称为frame的组件，作为Window的背景。该子组件通常扩展类型为“Label”。
+        2. frame的组件内部，一个名称为closeButton的按钮将自动作为窗口的关闭按钮。
+        3. frame的组件内部，一个名称为dragArea的图形(类型设置为空白)将自动作为窗口的检测拖动区域。
+        4. frame的组件内部，一个名称为contentArea的图形(类型设置为空白)将作为窗口的主要内容区域。
+    根据以上约定制作的Window，由各引擎内部的FairyGUI的相关包负责动态创建、渲染和事件响应。
+
+    Ren'Py中没有FairyGUI的包体处理以上内容，且通常不需要动态创建组件。
+    暂定按默认Component处理。可拖拽功能再议。
+    """
+    def __init__(self, component_etree, id, name, package_desc_id=None):
+        super().__init__(component_etree, id, name, package_desc_id=None)
+
+
+class FguiDisplayList:
+    """
+    FairyGUI组件内部显示列表，xml中displayList标签内容。
+    """
+    def __init__(self, display_list_etree, package_desc_id=None):
+        self.display_list_etree = display_list_etree
+        self.package_desc_id = package_desc_id
+        # print(f"package_desc_id: {package_desc_id}")
+        self.displayable_list = []
+        for displayable in self.display_list_etree:
+            print(displayable.tag)
+            # 根据标签类型创建相应的FguiDisplayable对象
+            if displayable.tag == "graph":
+                self.displayable_list.append(FguiGraph(displayable))
+            elif displayable.tag == "text":
+                self.displayable_list.append(FguiText(displayable))
+            elif displayable.tag == "image":
+                self.displayable_list.append(FguiImage(displayable))
+            elif displayable.tag == "list":
+                self.displayable_list.append(FguiList(displayable, self.package_desc_id))
+            elif displayable.tag == "loader":
+                self.displayable_list.append(FguiLoader(displayable))
+            else:
+                # 对于未知类型，创建基础的FguiDisplayable对象
+                self.displayable_list.append(FguiDisplayable(displayable))
+
+def hex_aarrggbb_to_rgba(hex_color):
+    """
+    将一个 8 位的十六进制颜色字符串 (AARRGGBB) 转换为一个 RGBA 元组。
+    """
+    # 移除字符串头部的 '#'
+    clean_hex = hex_color.lstrip('#')
+
+    # 检查处理后的字符串长度是否为 8
+    if len(clean_hex) != 8:
+        raise ValueError("输入的十六进制字符串必须是8位 (AARRGGBB)")
+
+    # 按 AARRGGBB 的顺序提取，并从16进制转换为10进制整数
+    try:
+        a = int(clean_hex[0:2], 16)
+        r = int(clean_hex[2:4], 16)
+        g = int(clean_hex[4:6], 16)
+        b = int(clean_hex[6:8], 16)
+    except ValueError:
+        raise ValueError("字符串包含无效的十六进制字符")
+
+    return (r, g, b, a)
+
+def rgba_normalize(rgba_tuple):
+    r = float(rgba_tuple[0]/255)
+    g = float(rgba_tuple[1]/255)
+    b = float(rgba_tuple[2]/255)
+    a = float(rgba_tuple[3]/255)
+    return (r, g, b, a)
+
+class ColorFilterData:
+    """
+    颜色滤镜数据，总共4项，分别为亮度、对比度、饱和度、色相。
+    """
+    def __init__(self, data_string):
+        if not data_string:
+            raise ValueError("Color Filter Data is Null.")
+        self.brightness, self.contrast, self.saturation, self.hue = map(float, data_string.split(","))
+
+
+class FguiDisplayable:
+    """
+    FairyGUI组件内显示对象。
+    可能的类型包括：graph(图形)、image(图片)、text(文字)、component(组件)、list(列表)、loader(装载器)。
+    基本属性：id、名称、引用源、位置、尺寸、缩放、倾斜、轴心、锚点、不透明度、旋转、是否可见、是否变灰、是否可触摸。
+
+    此外，fgui中的“效果”与“其他”配置也放在属性中。
+
+    支持多种子项。其中Button项仅限按钮作为子组件时才可能存在。
+    relation(暂未处理)和FGUI自身gearBase的派生类。
+    在一个FguiDisplayable对象中，以下各类gear均至多只存在一个。
+    除了 *是否显示* 可以由至多两个控制器决定，其他属性均只能由单一控制器决定。
+    gearBase的派生类如下：
+    gearDisplay-根据控制器决定是否显示；
+    gearDisplay2-协同另一个控制器决定是否显示；
+    gearLook-根据控制器决定外观transform，包括alpha、rotation、grayed、touchable等；
+    gearXY-根据控制器决定坐标；
+    gearSize-根据控制器决定尺寸，包括size和scale；
+    gearColor-根据控制器决定“图片-颜色”；
+    gearText-根据控制器决定文本组件显示的文本内容；
+    gearIcon-根据控制器决定装载器显示内容。
+    """
+    def __init__(self, display_item_tree, package_description_id=None):
+        self.display_item_tree = display_item_tree
+        # id
+        self.id = self.display_item_tree.get("id")
+        # 名称
+        self.name = self.display_item_tree.get("name")
+        # 引用源，通常是id。若为图片可在FguiAssets的package_desc.image_list中根据id查找图片名。若为组件可在FguiAssets的package_desc.component_list中根据id查找组件。
+        self.src = self.display_item_tree.get("src")
+        # 位置
+        xy = self.display_item_tree.get("xy", "0,0")
+        self.xypos = tuple(map(int, xy.split(",")))
+        # 尺寸。若为None：image的size默认与image对象一致。
+        size = self.display_item_tree.get("size")
+        self.size = tuple(map(int, size.split(","))) if size else None
+        # 保持比例。若为None，则将宽和高分别缩放到size。
+        self.aspect = (self.display_item_tree.get("aspect") == "true")
+        # 缩放。默认值为(1.0, 1.0)。
+        scale = self.display_item_tree.get("scale", "1.0,1.0")
+        self.scale = tuple(map(float, scale.split(",")))
+        # 倾斜。默认值为(0, 0)。
+        skew = self.display_item_tree.get("skew", "0,0")
+        self.skew = tuple(map(int, skew.split(",")))
+        # 轴心。默认值为(0.0, 0.0)。
+        pivot = self.display_item_tree.get("pivot", "0.0,0.0")
+        self.pivot = tuple(map(float, pivot.split(",")))
+        # 是否将轴心作为锚点。否认为False。
+        self.pivot_is_anchor = (self.display_item_tree.get("anchor") == "true")
+        # 不透明度。默认为1.0。
+        alpha = self.display_item_tree.get("alpha", "1.0")
+        self.alpha = float(alpha)
+        # 旋转。默认值为0。
+        rotation = self.display_item_tree.get("rotation", "0")
+        self.rotation = int(rotation)
+        # 未明确为不可见则默认可见
+        self.visible = not (self.display_item_tree.get("visible") == "false")
+        # 未明确是否变灰则默认不变灰
+        self.grayed = (self.display_item_tree.get("grayed") == "true")
+        # 未明确是否可触摸则默认可以触摸
+        self.touchable = not (self.display_item_tree.get("touchable") == "false")
+        # 资源包描述id，部分组件需要
+        self.package_description_id = package_description_id
+        # BlendMode
+        self.blend_mode = self.display_item_tree.get("blend", "normal")
+        # 滤镜
+        self.color_filter = self.display_item_tree.get("filter")
+        # 滤镜颜色变换
+        self.color_filter_values = ColorFilterData(self.display_item_tree.get("filterData", "1,1,1,1"))
+        # Tooltips，一般指指针悬垂在组件上时显示的说明文本。
+        self.tooltips = self.display_item_tree.get("tooltips")
+        # 自定义数据
+        self.custom_data = self.display_item_tree.get("customData")
+
+        # Button，按钮专有属性
+        self.button_property = None
+
+        # gear属性
+        self.gear_display = None
+        self.gear_display_2 = None
+        self.gear_pos = None
+        self.gear_look = None
+        self.gear_size = None
+        self.gear_color = None
+        self.gear_text = None
+        self.gear_icon = None
+
+        # 一级子对象
+        self.child_num = len(self.display_item_tree)
+        # 控制器gear子组件
+        for i in range(self.child_num):
+            if self.display_item_tree[i].tag == "gearDisplay" :
+                self.gear_display = FguiGearDisplay(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearDisplay2" :
+                self.gear_display_2 = FguiGearDisplay(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearXY" :
+                self.gear_pos = FguiGearPos(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearSize" :
+                self.gear_size = FguiGearSize(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearLook" :
+                self.gear_look = FguiGearLook(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearColor" :
+                self.gear_color = FguiGearColor(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearText" :
+                self.gear_text = FguiGearText(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "gearIcon" :
+                self.gear_icon = FguiGearIcon(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "Button" :
+                self.button_property = FguiButtonProperty(self.display_item_tree[i])
+            else:
+                print(f"Tag not parse: {self.display_item_tree[i].tag}.")
+
+    def __repr__(self):
+        return f"FguiDisplayable({self.id}, {self.name}, {self.xypos}, {self.size}, \
+{self.scale}, {self.skew}, {self.pivot}, {self.pivot_is_anchor}, {self.alpha}, \
+{self.rotation}, {self.visible}, {self.grayed}, {self.touchable})"
+
+class FguiComponentPropertyBase:
+    """
+    组件子属性信息基类
+    """
+    def __init__(self, component_property_tree):
+        self.component_property_tree = component_property_tree
+        self.property_name = self.component_property_tree.tag
+
+class FguiButtonProperty(FguiComponentPropertyBase):
+    """
+    组件子属性中的Button信息。
+    通常包括title和icon，分别表示标题(文本)和图标(装载器)
+    """
+    def __init__(self, component_property_tree):
+        super().__init__(component_property_tree)
+        if self.property_name != "Button" :
+            raise ValueError("xml tag is not Button")
+        self.title = self.component_property_tree.get("title")
+        self.selected_title = self.component_property_tree.get("selectedTitle")
+        self.icon = self.component_property_tree.get("icon")
+        self.selected_icon = self.component_property_tree.get("selectedIcon")
+
+
+class FguiGraph(FguiDisplayable):
+    """
+    FairyGUI中的图形。包括空白、矩形(圆边矩形)、圆形(椭圆)、多边形等。
+    """
+    def __init__(self, display_item_tree):
+        if display_item_tree.tag != "graph" :
+            raise ValueError("xml tag is not graph.")
+        super().__init__(display_item_tree)
+        # None: 空白  rect: 矩形(可带圆角) eclipse: 椭圆(包括圆形)  regular_polygon: 正多边形  polygon: 多边形
+        self.type = self.display_item_tree.get("type", None)
+        self.stroke_width = int(self.display_item_tree.get("lineSize", "1"))
+        stroke_color= self.display_item_tree.get("lineColor", "#ff000000") # 描边默认为黑色
+        self.stroke_color  = hex_aarrggbb_to_rgba(stroke_color)
+        fill_color = self.display_item_tree.get("fillColor", "#ffffffff") # 描边默认为白色
+        self.fill_color = hex_aarrggbb_to_rgba(fill_color)
+        # 矩形可能存在圆角
+        self.corner_radius = int(self.display_item_tree.get("corner", "0"))
+        # 正多边形需要记录边数和顶点位置。
+        # 顶点位置使用一个数组表示，数组长度等于顶点数(边数)。
+        # 顶点只能存在于标准正多边形顶点到图形中心的连线上。
+        # 每个数字表示对应顶点到图形中心的距离，最大值为1.0，最小值为0.0。
+        if (self.type == "regular_polygon"):
+            self.sides =  int(self.display_item_tree.get("sides", "3"))
+            distances = self.display_item_tree.get("distances", "1.0,"*(self.sides-1)+"1.0")
+            distances_list = distances.split(",")
+            for i in range(len(distances_list)):
+                if distances_list[i] == '':
+                    distances_list[i] = "1.0"
+        # 多边形只记录顶点坐标。
+        # points为顺序连接的一组xy坐标，例如"5,4,12,-2,20.78191,5.746792,14,16,3,14"。
+        # 虽然发布的资源xml中坐标是浮点，但FairyGUI编辑器却显示为整型。可考虑转换时也缩小精度为整型。
+        if (self.type == "polygon"):
+            point_list = self.display_item_tree.get("points", "0.0,0.0").split(",")
+            iteration = iter(point_list)
+            self.points = tuple((float(x), float(y)) for x, y in zip(iteration, iteration))
+
+class FguiText(FguiDisplayable):
+    """
+    FairyGUI中的文本。
+    """
+    def __init__(self, display_item_tree):
+        if display_item_tree.tag != "text" :
+            raise ValueError("xml tag is not text.")
+        super().__init__(display_item_tree)
+        # FairyGUI编辑器未设置全局字体的情况下，默认渲染使用Arial，与Ren'Py默认字体SourceHanSansLite不同。
+        # 此处的字体与FairyGUI编辑器中的字体属性不同，为字体名称。编辑器中的xml则记录引用项。
+        # 另外，FairyGUI发布的资源文件中并不包含字体文件，需要手工放置到游戏引擎对应目录。
+        self.text = self.display_item_tree.get("text", "")
+        self.font = self.display_item_tree.get("font")
+        self.font_size = int(self.display_item_tree.get("fontSize", 24) )
+        self.text_color = self.display_item_tree.get("color", "#000000")
+        self.align = self.display_item_tree.get("align", "left") # 共有left、center、right三种
+        self.v_align = self.display_item_tree.get("vAlign", "top") # 共有top、middle、bottom三种
+        self.ubb = (self.display_item_tree.get("ubb") == "true") # UBB语法暂不考虑
+        self.auto_size = self.display_item_tree.get("autoSize")
+        self.letter_spacing = self.display_item_tree.get("letterSpacing", 0) # 字间距
+        self.leading = self.display_item_tree.get("leading", 3) # 行间距
+        self.underline = (self.display_item_tree.get("underline") == "true")
+        self.bold = (self.display_item_tree.get("bold") == "true")
+        self.italic = (self.display_item_tree.get("italic") == "true")
+        self.strike = (self.display_item_tree.get("strike") == "true")
+        self.single_line = (self.display_item_tree.get("singleLine") == "true")
+        self.stroke_color = self.display_item_tree.get("strokeColor")
+        self.stroke_size = self.display_item_tree.get("strokeSize", 1)
+        self.shadow_color = self.display_item_tree.get("shadowColor")
+        shadow_offset = self.display_item_tree.get("shadowOffset")
+        if shadow_offset:
+            self.shadow_offset = tuple(map(int, shadow_offset.split(",")))
+        else:
+            self.shadow_offset = (0, 0)
+        # 下面几项仅限输入框
+        self.is_input = (self.display_item_tree.get("input") == "true")
+        self.prompt = self.display_item_tree.get("prompt")
+        self.max_length = int(self.display_item_tree.get("maxLength", 0))
+        self.restrict = self.display_item_tree.get("restrict") #输入文本规则，正则表达式字符串
+        self.is_password = (self.display_item_tree.get("password") == "true")
+        
+
+
+class FguiImage(FguiDisplayable):
+    """
+    FairyGUI中的图片。
+    自身没有什么独特属性。
+    """
+    def __init__(self, display_item_tree):
+        if display_item_tree.tag != "image" :
+            raise ValueError("xml tag is not image.")
+        super().__init__(display_item_tree)
+
+class FguiListItem():
+    """
+    列表元素，仅会在list内部。
+    tag为item。
+    属性如下：
+    url-引用资源url，格式为“ui://”+“packageDescription id” + “component id”。若为空表示使用列表默认元素。
+    title：标题，通常用于按钮。
+    icon：图标，通常用于按钮。
+    name：名称，没用。
+    样例：
+    <item url="ui://jlqvyvtyspct6" title="开坑" icon="ui://jlqvyvty9m682" name="buttonname"/>
+    """
+    def __init__(self, list_item_tree, package_description_id=None):
+        if list_item_tree.tag != "item":
+            raise ValueError("xml tag is not item.")
+        self.list_item_tree = list_item_tree
+        self.package_description_id = package_description_id
+        self.item_url = self.list_item_tree.get("url")
+        self.item_title = self.list_item_tree.get("title")
+        self.item_icon = self.list_item_tree.get("icon")
+        self.item_name = self.list_item_tree.get("name")
+    def __repr__(self):
+        return f"FguiListItem({self.item_url}, {self.item_title}, {self.item_icon}, {self.item_name})"
+
+
+class FguiList(FguiDisplayable):
+    """
+    FairyGUI中的列表。
+    列表存在“树视图”，暂不考虑。
+    列表的属性如下：
+    layout-列表布局：(column)默认-单列竖排，row-单行横排，flow_hz-横向流动，flow_vt-纵向流动、pagination-分页
+    overflow-溢出处理：(visible)默认-可见，hidden-隐藏，scroll-滚动
+    scroll-滚动条方向：(vertical)默认-垂直滚动，horizontal-水平滚动，both-自由滚动(同时允许垂直滚动和水平滚动)
+    scrollBar-显示滚动条：visible-可见，hidden-隐藏，auto-滚动时显示
+    scrollBarFlags：一系列滚动条标识位，可能是一个12bit整数。从低位到高位分别为：
+        0-bit：垂直滚动条显示在左边
+        1-bit：滚动位置自动贴近元件
+        2-bit：仅在内容溢出时才显示滚动条
+        3-bit：页面模式
+        4-bit 5-bit：触摸滚动效果，00默认、01启用、10关闭
+        6-bit 7-bit：边缘回弹效果，00默认、01启用、10关闭
+        8-bit：禁用惯性
+        9-bit：禁用剪裁
+        10-bit：浮动显示
+        11-bit：禁用剪裁边缘
+    当列表允许滚动时，有一大堆特性暂不处理，例如“边缘回弹”、指定滚动条组件、自动贴近元件等。
+    margin：边缘留空，4个整数，分别对应上下左右。
+    clipSoftness：边缘虚化，xy分辨对应水平与垂直方向的虚化程度。
+    lineItemCount：列表布局为横向流动或分页时，表示列数。列表布局为竖向流动时，表示行数。其他布局中，该参数无效果。
+    lineItemCount2：列表布局为分页时，表示行数。其他布局中，该参数无效果。
+    lineGap：行距。
+    colGap：列距。
+    defaultItem：默认元素，通常是一个资源url，格式为“ui://”+“packageDescription id” + “component id”。
+    """
+    def __init__(self, display_item_tree, package_description_id=None):
+        if display_item_tree.tag != "list" :
+            raise ValueError("xml tag is not list.")
+        super().__init__(display_item_tree)
+        self.layout = self.display_item_tree.get("layout", "column")
+        self.overflow = self.display_item_tree.get("overflow", "visible")
+        self.scroll = self.display_item_tree.get("scroll", "vertical")
+        self.scroll_bar_flags = int(self.display_item_tree.get("scrollBarFlags", "256"))
+        margin = self.display_item_tree.get("margin")
+        self.margin = tuple(map(int, margin.split(","))) if margin else None
+        self.clip_softness = self.display_item_tree.get("clipSoftness", "0,0")
+        self.line_item_count = int(self.display_item_tree.get("lineItemCount", "1"))
+        self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "1"))
+        self.line_gap = int(self.display_item_tree.get("lineGap", "0"))
+        self.col_gap = int(self.display_item_tree.get("colGap", "0"))
+        self.default_item_url = self.display_item_tree.get("defaultItem")
+        self.default_item_id = None
+        self.package_description_id = package_description_id
+        if package_description_id:
+            self.get_default_item(package_description_id)
+        self.item_list = []
+        for item_tree in display_item_tree:
+            item = FguiListItem(item_tree, self.package_description_id)
+            self.item_list.append(item)
+
+    def get_default_item(self, packageDescription_id):
+        self.default_item_id = self.default_item_url[self.default_item_url.find(packageDescription_id)+len(packageDescription_id):]
+
+
+class FguiLoader(FguiDisplayable):
+    """
+    FairyGUI中的装载器。
+    包含属性url：表示引用的组件url，通常是一个资源url，格式为“ui://”+“packageDescription id” + “component id”。
+    """
+    def __init__(self, display_item_tree, package_description_id=None):
+        if display_item_tree.tag != "loader" :
+            raise ValueError("xml tag is not loader.")
+        super().__init__(display_item_tree)
+        self.url = self.display_item_tree.get("url")
+        self.item_url = None
+        if package_description_id:
+            self.package_description_id = package_description_id
+            get_item_id(package_description_id)
+
+    def get_item_id(packageDescription_id):
+        self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):]
+
+class FguiGearBase:
+    """
+    Displayable控制器设置相关的基类。
+    必定包含controller属性。
+    可能包含page、value、default、tween属性。
+    controller: 相关控制器名。
+    page: 相关控制器索引。可能存在多个索引值，使用逗号分隔。
+    value：与控制器索引对应的值，具体格式和作用根据Gear类型决定。若存在多个值则使用“|”分割。
+    default：默认值。page属性未列出的控制器索引使用该默认值。
+    tween：是否启用缓动。
+    """
+    def __init__(self, gear_item_tree):
+        self.gear_item_tree = gear_item_tree
+        self.controller_name = gear_item_tree.get("controller")
+        self.controller_index = None
+        controller_index = gear_item_tree.get("pages")
+        values = gear_item_tree.get("values")
+        if controller_index:
+            self.controller_index = controller_index.split(",")
+        self.values = None
+        if values:
+            self.values = values.split("|")
+        self.default = gear_item_tree.get("default")
+        self.tween = True if (gear_item_tree.get("tween") == "true") else False
+
+
+class FguiGearDisplay(FguiGearBase):
+    """
+    Displayable中控制器与显示相关的设置。
+    只在指定控制器的索引等于指定索引时才会显示displayable。
+    唯一至多存在2个的gear，tag名称分别为gearDisplay、gearDisplay2。
+    可由两个控制器同时控制是否显示。
+    两个控制器的控制逻辑可以是“与”，或者“或”。
+    例：
+    <gearDisplay controller="button" pages="0"/>
+    <gearDisplay2 controller="button2" pages="0" condition="0"/>
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearDisplay" and gear_item_tree.tag != "gearDisplay2":
+            raise ValueError(f"xml tag is {gear_item_tree.tag}, not gearDisplay.")
+        super().__init__(gear_item_tree)
+        # condition=0——“与”逻辑；condition=1——“或”逻辑
+        condition = gear_item_tree.get("condition")
+        self.condition = 0
+        if condition:
+            self.condition = int(condition)
+
+class FguiGearPos(FguiGearBase):
+    """
+    Displayable中控制器与位置相关的设置。
+    属性values的值与属性pages有关。
+    若pages只有一个控制值索引，value是一个使用竖线 ‘|’ 连接的 ‘x,y’形式坐标列表。
+    若pages包含多个控制器索引，values则是多个固定长度2列表，使用竖线 ‘|’ 连接。
+    例：
+    <gearXY controller="ClassController" pages="0,1" values="807,241|823,241"/>
+    <gearXY controller="button" default="60,50" tween="true"/>
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearXY" :
+            raise ValueError("xml tag is not gearXY.")
+        super().__init__(gear_item_tree)
+        value = gear_item_tree.get("value")
+        self.index_value_dict = {} # 该字典存放控制器索引与坐标
+        if self.values:
+            for i in range(len(self.values)):
+                xypos = tuple(map(int, self.values[i].split(",")))
+                self.index_value_dict[self.controller_index[i]] = xypos
+        if self.default:
+            xypos = tuple(map(int, self.default.split(",")))
+            self.index_value_dict["default"] = xypos
+
+class FguiGearLook(FguiGearBase):
+    """
+    Displayable中控制器与外观相关的设置。
+    属性values的值与属性pages有关。
+    若pages只有一个控制值索引，values是一个使用逗号连接的固定长度4列表，分别对应透明度、旋转、变灰、不可触摸。
+    若pages包含多个控制器索引，values则是多个固定长度4列表，使用竖线 ‘|’ 连接。
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearLook" :
+            raise ValueError("xml tag is not gearLook.")
+        super().__init__(gear_item_tree)
+        self.index_value_dict = {} # 该字典存放控制器索引与对应透明度、旋转、变灰、不可触摸
+        if self.values:
+            for i in range(len(self.values)):
+                item = self.values[i].split(",")
+                alpha = float(item[0])
+                rotation = int(item[1])
+                grayed = False if (item[2] != '0') else True
+                touchable = True if (item[3] == '1') else False
+                self.index_value_dict[self.controller_index[i]] = (alpha, rotation, grayed, touchable)
+        if self.default:
+            item = self.default.split(",")
+            alpha = float(item[0])
+            rotation = int(item[1])
+            grayed = False if (item[2] != '0') else True
+            touchable = True if (item[3] == '1') else False
+            self.index_value_dict["default"] = (alpha, rotation, grayed, touchable)
+
+class FguiGearSize(FguiGearBase):
+    """
+    Displayable中控制器与尺寸相关的设置。
+    属性values的值与属性pages有关。
+    若pages只有一个控制值索引，values是一个使用逗号连接的固定长度4列表，分别对应宽度、高度、宽度缩放系数、高度缩放系数。
+    若pages包含多个控制器索引，values则是多个固定长度4列表，使用竖线 ‘|’ 连接。
+    例：
+    <gearSize controller="button" default="36,19,1,1"/>
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearSize" :
+            raise ValueError("xml tag is not gearSize.")
+        super().__init__(gear_item_tree)
+        self.index_value_dict = {} # 该字典存放控制器索引与对应尺寸
+        if self.values:
+            for i in range(len(self.values)):
+                item = index_values[i].split(",")
+                width = int(item[0])
+                height = int(item[1])
+                xscale = float(item[2])
+                yscale = float(item[3])
+                self.index_value_dict[self.controller_index[i]] = (width, height, xscale, yscale)
+        if self.default:
+            item = self.default.split(",")
+            width = int(item[0])
+            height = int(item[1])
+            xscale = float(item[2])
+            yscale = float(item[3])
+            self.index_value_dict["default"] = item
+
+class FguiGearColor(FguiGearBase):
+    """
+    Displayable中控制器与一个与图像颜色相乘的颜色设置。默认为“#ffffff”，白色，相乘后无视觉变化。
+    属性values的值与属性pages有关。
+    若pages只有一个控制值索引，values是一个24位的十六进制颜色值。
+    若pages包含多个控制器索引，values则是多个十六进制颜色值，使用竖线 ‘|’ 连接。
+    例:
+    <gearColor controller="button" pages="1,2,3" values="#cccccc|#999999|#333333" default="#ffffff"/>
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearColor" :
+            raise ValueError("xml tag is not gearColor.")
+        super().__init__(gear_item_tree)
+        self.index_value_dict = {} # 该字典存放控制器索引与对应颜色
+        if self.values:
+            for i in range(len(self.values)):
+                self.index_value_dict[self.controller_index[i]] = self.values[i]
+        if self.default:
+            self.index_value_dict["default"] = self.default
+
+class FguiGearText(FguiGearBase):
+    """
+    只会出现在文本组件中才生效的控制属性。
+    属性values的值与属性pages有关。
+    若pages只有一个控制值索引，values是一个字符串。
+    若pages包含多个控制器索引，values则是字符串，使用竖线 ‘|’ 连接。
+    例:
+    <gearText controller="ClassController" pages="0,1" values="软骨鱼纲|辐鳍鱼纲"/>
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearText" :
+            raise ValueError("xml tag is not gearText.")
+        super().__init__(gear_item_tree)
+        self.index_value_dict = {} # 该字典存放控制器索引与对应文本
+        if self.values:
+            for i in range(len(self.values)):
+                self.index_value_dict[self.controller_index[i]] = self.values[i]
+        if self.default:
+            self.index_value_dict["default"] = self.default
+
+class FguiGearIcon(FguiGearBase):
+    """
+    只会出现在装载器组件中才生效的控制属性。
+    属性values的值与属性pages有关。
+    若pages只有一个控制值索引，values是一个资源url，格式为“ui://”+“packageDescription id” + “component id”。
+    若pages包含多个控制器索引，values是多个资源url，使用竖线 ‘|’ 连接。
+    例:
+    <gearIcon controller="button" pages="1" values="ui://gs4t1m2tno2rz" default="ui://gs4t1m2tno2r10"/>
+    """
+    def __init__(self, gear_item_tree):
+        if gear_item_tree.tag != "gearIcon" :
+            raise ValueError("xml tag is not gearIcon.")
+        super().__init__(gear_item_tree)
+        self.index_value_dict = {} # 该字典存放控制器索引与显示内容
+        if self.values:
+            for i in range(len(self.values)):
+                self.index_value_dict[self.controller_index[i]] = self.values[i]
+        if self.default:
+            self.index_value_dict["default"] = self.default
+
+class FguiAssets():
+    """
+    资源解析入口。
+    """
+    def __init__(self, fgui_project_path):
+        if not fgui_project_path:
+            raise ValueError("Project path is illegal.")
+        self.fgui_project_name = os.path.basename(fgui_project_path)
+        self.package_desc_file = os.path.join(fgui_project_path, f"{self.fgui_project_name}.bytes")
+        # 发布的描述文件
+        self.package_desc = None
+        self.object_dict = ParseFguiPackageDescFile(self.package_desc_file)
+        # 图集和图像描述文件
+        self.sprite_desc_file = os.path.join(fgui_project_path, f"{self.fgui_project_name}@sprites.bytes")
+        self.fgui_image_set = []
+        self.fgui_atlas_dicts = {}
+        # 组件信息
+        self.fgui_component_set = []
+
+        # 先找到packageDescription，解析出component、image和atlas列表
+        package_key = 'package'
+        if (not self.object_dict.__contains__(package_key)):
+            raise ValueError('Could not find package description.')
+        package_value = self.object_dict.get(package_key)
+        if self.package_desc:
+            self.package_desc.clear()
+        self.package_desc = FguiPackage(package_value)
+        print("This package includes", len(self.package_desc.component_list), "component(s).")
+        for component in self.package_desc.component_list:
+            if (not self.object_dict.__contains__(component.id)):
+                raise ValueError('Could not find component info.')
+            extention_type = self.object_dict[component.id].get("extention")
+            # 根据extention构造不同对象
+            if extention_type == "Button":
+                component = FguiButton(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+            elif extention_type == "ScrollBar":
+                component = FguiScrollBar(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+            elif extention_type == "Label":
+                component = FguiLabel(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+            elif extention_type == "Slider":
+                component = FguiSlider(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+            else:
+                component = FguiComponent(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+            self.fgui_component_set.append(component)
+        # 根据atlas_list建立altas_id与实际图集文件间的映射关系
+        for atlas in self.package_desc.atlas_list:
+            atlas_file_name = self.fgui_project_name + '@' + atlas.file
+            self.fgui_atlas_dicts[atlas.id] = atlas_file_name
+        # 若不需要从atlas切割出单个图片，可以结合 *@sprites.bytes文件，获取每个image对象
+        self.fgui_image_set = ParseFguiSpriteDescFile(self.sprite_desc_file)
+
+    def clear(self):
+        self.fgui_project_name = ''
+        self.package_desc.clear()
+        self.object_dict.clear()
+        self.fgui_atlas_dicts.clear()
+        self.fgui_component_set.clear()
+
+    def get_componentname_by_id(self, id):
+        return self.package_desc.id_name_mapping[id]
+
+    def get_component_by_id(self, id):
+        for component in self.fgui_component_set:
+            if component.id == id:
+                return component
+        return None
+
+    def get_image_size_by_id(self, id):
+        for image in self.fgui_image_set:
+            if image.image_id == id:
+                return (image.width, image.height)
diff --git a/src/fgui_converter/__init__.py b/src/fgui_converter/__init__.py
new file mode 100644
index 0000000..385d8b7
--- /dev/null
+++ b/src/fgui_converter/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+# 将该目录视为可导入的 Python 包。
+
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
new file mode 100644
index 0000000..0508eff
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -0,0 +1,999 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+import re
+import argparse
+
+import shutil
+
+# 添加当前目录到Python路径，以便导入FguiAssetsParseLib
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+
+from fgui_converter.FguiAssetsParseLib import *
+
+class FguiToRenpyConverter:
+    """
+    FairyGUI到Ren'Py转换器
+    将FairyGUI资源转换为Ren'Py的screen语言
+    """
+
+    def __init__(self, fgui_assets):
+        self.fgui_assets = fgui_assets
+        self.renpy_code = []
+        self.screen_code = []
+        self.screen_definition_head = []
+        self.screen_variable_code = []
+        self.screen_function_code = []
+        self.screen_ui_code = []
+        self.style_code = []
+        
+        # dismiss用于取消一些组件的focus状态，例如input。
+        self.screen_has_dismiss = False
+        self.dismiss_action_list = []
+
+        # 文本对齐的转义字典
+        self.align_dict = {"left": 0.0, "center": 0.5, "right": 1.0, "top": 0.0, "middle": 0.5, "bottom": 1.0}
+
+        # 字体名称列表。SourceHanSansLite为Ren'Py默认字体。
+        self.font_name_list = ["SourceHanSansLite"]
+
+        # 4个空格作为缩进基本单位
+        self.indent_unit = '    '
+        # 组件缩进级别
+        self.root_indent_level = 0
+        # 缩进字符串
+        self.indent_str = ''
+
+        # 默认背景色，在某些未指定image的情况下用作填充。
+        self.default_background = '#fff'
+
+        # 部分模板与预设目录
+        # self.renpy_template_dir = 'renpy_templates'
+        self.renpy_template_dir = os.environ.get('RENPY_TEMPLATES_DIR', 
+                             os.path.join(os.path.dirname(os.path.abspath(__file__)), "renpy_templates"))
+        self.font_map_template = 'renpy_font_map_definition.txt'
+        self.graph_template_dict = {}
+        self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt')
+        self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt')
+
+    def calculate_indent(self):
+        self.indent_str = self.indent_unit * self.root_indent_level
+        return self.indent_str
+
+    def indent_level_up(self, levelup=1):
+        self.root_indent_level += levelup
+        self.indent_str = self.indent_unit * self.root_indent_level
+
+    def indent_level_down(self, leveldown=1):
+        self.root_indent_level = max(self.root_indent_level-leveldown, 0)
+        self.indent_str = self.indent_unit * self.root_indent_level
+
+    def reset_indent_level(self, indent_level=0):
+        self.root_indent_level = indent_level
+        self.indent_str = ''
+
+    def generate_image_definitions(self):
+        """生成图像定义"""
+        image_definitions = []
+        image_definitions.append("# 图像定义")
+        image_definitions.append("# 从FairyGUI图集中提取的图像")
+
+        for sprite in self.fgui_assets.fgui_image_set:
+            # 找到对应的图像信息
+            image_info = None
+            image_name = ''
+            image_scale = None
+            image_scale9grid = None
+            for img in self.fgui_assets.package_desc.image_list:
+                if img.id == sprite.image_id:
+                    image_info = sprite
+                    image_name = img.name
+                    image_scale = img.scale
+                    image_scale9grid = img.scale9grid
+                    break
+
+            # GetOriImage(image_id, package_desc, fgui_image_sets, fgui_atlas_dicts)
+
+            if image_info:
+                atlas_index = image_info.atlas_index
+                atlas_key = f"atlas{atlas_index}"
+                atlas_file = self.fgui_assets.fgui_atlas_dicts[atlas_key]
+                # 计算在图集中的位置
+                x, y = sprite.x, sprite.y
+                width, height = sprite.width, sprite.height
+
+                # 生成Ren'Py图像定义
+                # 由于Ren'Py中文件名带 @ 表示过采样，替换为下划线 _
+                atlas_file = atlas_file.replace('@', '_').lower()
+                image_name = image_name.replace('@', '_')
+                # 九宫格
+                if image_scale == "9grid":
+                    ima_str = f'im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))'
+                    # FGUI中的border是相对x轴和y轴的偏移量，需要根据尺寸再计算为宽度或高度
+                    left = int(image_scale9grid[0])
+                    top = int(image_scale9grid[1])
+                    right = width - left - int(image_scale9grid[2])
+                    bottom = height - top - int(image_scale9grid[3])
+                    border_str = f"{left}, {top}, {right}, {bottom}"
+                    image_definitions.append(f'image {image_name} = Frame({ima_str}, {border_str})')
+                # 平铺
+                elif image_scale == "tile":
+                    # image bg tile = Tile("bg.png")
+                    ima_str = f'im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))'
+                    image_definitions.append(f'image {image_name} = Tile({ima_str})')
+                # 无拉伸普通图片
+                else:
+                    image_definitions.append(f'image {image_name} = im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))')
+
+        image_definitions.append("")
+        self.renpy_code.extend(image_definitions)
+
+    def generate_screen(self, component):
+        """
+        生成screen定义。目标样例：
+
+        screen test_main_menu():
+            add 'menu_bg':
+                pos (0, 0)
+
+            fixed:
+                pos (1007, 178)
+                use main_menu_button(title='开坑', actions=ShowMenu("save"))
+            fixed:
+                pos (1007, 239)
+                use main_menu_button(title='填坑', actions=ShowMenu("load"))
+            fixed:
+                pos (1007, 300)
+                use main_menu_button(title='设置', actions=ShowMenu("preferences"))
+            fixed:
+                pos (1007, 361)
+                use main_menu_button(title='关于', actions=ShowMenu("about"))
+            fixed:
+                pos (1007, 422)
+                use main_menu_button(title='帮助', actions=ShowMenu("help"))
+            fixed:
+                pos (1007, 483)
+                use main_menu_button(title='放弃', actions=Quit())
+        """
+        self.screen_code.clear()
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+
+        self.screen_definition_head.append("# 组件Screen")
+        self.screen_definition_head.append(f"# 从FairyGUI组件{component.name}转换而来")
+
+        id = component.id
+        screen_name = component.name
+
+        self.reset_indent_level()
+        self.screen_definition_head.append(f"screen {screen_name}():")
+        self.indent_level_up()
+        for displayable in component.display_list.displayable_list:
+            # 图片组件
+            if isinstance(displayable, FguiImage):
+                self.screen_ui_code.extend(self.generate_image_displayable(displayable))
+            # 图形组件
+            elif isinstance(displayable, FguiGraph):
+                self.screen_ui_code.extend(self.generate_graph_displayable(displayable))
+            # 文本组件
+            elif isinstance(displayable, FguiText):
+                self.screen_ui_code.extend(self.generate_text_displayable(displayable))
+            # 列表
+            elif isinstance(displayable, FguiList):
+                self.screen_ui_code.extend(self.generate_list_displayable(displayable))
+            # 装载器
+            elif isinstance(displayable, FguiLoader):
+                pass
+            # 其他组件
+            else:
+                # 根据引用源id查找组件
+                ref_com = self.fgui_assets.get_component_by_id(displayable.src)
+                # 按钮。可设置标题，并根据自定义数据字段设置action。
+                if ref_com.extention == "Button" and ref_com.name != None:
+                    self.screen_ui_code.append(f"{self.indent_str}fixed:")
+                    self.indent_level_up()
+                    self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data)
+                    self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id \"{displayable.id}\"")
+                    self.indent_level_down()
+
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_variable_code)
+        self.screen_code.append("")
+        self.screen_code.extend(self.screen_function_code)
+        self.screen_code.append("")
+        # 添加只有1个生效的dismiss
+        if self.screen_has_dismiss:
+            self.screen_ui_code.append(f"{self.indent_str}dismiss:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}modal False")
+            dismiss_action_list = ', '.join(self.dismiss_action_list)
+            self.screen_ui_code.append(f"{self.indent_str}action [{dismiss_action_list}]")
+            
+        self.screen_code.extend(self.screen_ui_code)
+
+    def generate_button_parameter(self, button_title=None, original_actions_str=None):
+        parameter_str = ""
+        title_str = ""
+        actions_str = ""
+        if button_title :
+            title_str = f"title=\'{button_title}\'".replace("\n", "\\n").replace("\r", "\\n")
+        if original_actions_str :
+            actions_str = f"actions={original_actions_str}"
+        if button_title and original_actions_str:
+            parameter_str = f"{title_str}, {actions_str}"
+        else:
+            parameter_str = title_str if title_str else actions_str
+
+        return parameter_str
+
+    def generate_text_style(self, fgui_text, style_name):
+        """
+        生成文本样式，专用于按钮标题，因为按钮中通常只有一个文本组件。
+        目标样例：
+
+        style main_menu_button_text:
+            align (0.5, 0.5)
+            font "hyzjhj.ttf"
+            color "#FEDAAA"
+            size 32
+            outlines [(absolute(1), "#C0C0C0", absolute(3), absolute(3)), (absolute(1), "#FAB5A4", absolute(0), absolute(0))]
+            textalign 0.5
+        """
+
+        self.style_code.clear()
+        # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
+        if not isinstance(fgui_text, FguiText):
+            print("It is not a text displayable.")
+            return
+        # 样式具有固定一档的缩进
+        style_indent = "    "
+        default_title = fgui_text.text
+        # 定义样式
+        self.style_code.append(f"style {style_name}:")
+        self.style_code.append(f"{style_indent}xysize {fgui_text.size}")
+        # 字体可能为空，改为Ren'Py内置默认字体SourceHanSansLite
+        text_font =  fgui_text.font if fgui_text.font else "SourceHanSansLite"
+        # 此处的字体名缺少后缀，在Ren'Py中直接使用会报错。
+        # 需将字体名添加到font_name_list中，并替换renpy_font_map_definition模板中对应内容。
+        if not text_font in self.font_name_list:
+            self.font_name_list.append(text_font)
+        self.style_code.append(f"{style_indent}font \"{text_font}\"")
+        self.style_code.append(f"{style_indent}size {fgui_text.font_size}")
+        self.style_code.append(f"{style_indent}color \"{fgui_text.text_color}\"")
+        xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
+        self.style_code.append(f"{style_indent}align ({xalign}, {yalign})")
+        # Ren'Py中使用两层outlines分别实现投影和描边
+        # FGUI中的投影包含描边，投影的size等于描边的size，默认值为0；Ren'Py中允许outlines为0，依然有效果
+        # Ren'Py中的property outlines可以是列表，先投影后描边
+        has_shadow = fgui_text.shadow_color
+        shadow_width = fgui_text.stroke_size
+        has_outline = fgui_text.stroke_color
+        if has_shadow:
+            shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+        if has_outline:
+            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+        if has_shadow and not has_outline:
+            self.style_code.append(f"{style_indent}outlines [{shadow_outline}]")
+        if not has_shadow and has_outline:
+            self.style_code.append(f"{style_indent}outlines [{stroke_outline}]")
+        if has_shadow and has_outline:
+            self.style_code.append(f"{style_indent}outlines [{shadow_outline}, {stroke_outline}]")
+        # 默认两侧居中对齐
+        self.style_code.append(f"{style_indent}textalign 0.5")
+        # 粗体、斜体、下划线、删除线
+        if fgui_text.bold:
+            self.style_code.append(f"{style_indent}bold {fgui_text.bold}")
+        if fgui_text.italic:
+            self.style_code.append(f"{style_indent}italic {fgui_text.italic}")
+        if fgui_text.underline:
+            self.style_code.append(f"{style_indent}underline {fgui_text.underline}")
+        if fgui_text.strike:
+            self.style_code.append(f"{style_indent}strikethrough {fgui_text.strike}")
+        self.style_code.append("")
+
+    def generate_button_screen(self, component):
+
+        """
+        生成按钮组件screen。目标样例：
+
+        screen main_menu_button(title='', actions=NullAction()):
+            button:
+                xysize (273, 61)
+                style_prefix 'main_menu_button'
+                background 'main_menu_button_bg'
+                text title:
+                    align (0.5, 0.5)
+                action actions
+
+        style main_menu_button_text:
+            align (0.5, 0.5)
+            font "hyzjhj.ttf"
+            color "#FEDAAA"
+            size 32
+            outlines [(absolute(1), "#C0C0C0", absolute(3), absolute(3)), (absolute(1), "#FAB5A4", absolute(0), absolute(0))]
+            textalign 0.5
+        """
+
+        self.screen_code.clear()
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.has_dismiss = False
+        self.dismiss_action_list.clear()
+        self.style_code.clear()
+
+        if component.extention != 'Button':
+            print("组件类型不是按钮")
+            return
+
+        # 生成按钮组件screen
+        self.screen_definition_head.append("# 按钮组件Screen")
+        self.screen_definition_head.append(f"# 从FairyGUI按钮组件{component.name}转换而来")
+        # screen_code.append("")
+
+        id = component.id
+        button_name = component.name
+        xysize = component.size
+        background = self.default_background
+        default_title = ''
+        for displayable in component.display_list.displayable_list:
+            # 图片组件
+            if isinstance(displayable, FguiImage):
+                for image in self.fgui_assets.package_desc.image_list:
+                    if displayable.src == image.id:
+                        background = image.name
+                        break
+                    # TODO
+                    # 处理不同图片分别用于idle、hover、selected_idle和selected_hover的情况
+            # 文本组件
+            # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
+            # if isinstance(displayable, FguiText) and displayable.name == 'title':
+            if isinstance(displayable, FguiText):
+                # 重置缩进级别
+                self.reset_indent_level()
+                default_title = displayable.text
+                # 定义样式
+                self.generate_text_style(displayable, f"{button_name}_text")
+                # self.screen_code.extend(self.style_code)
+
+        # 重置缩进级别
+        self.reset_indent_level()
+        self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions=NullAction()):")
+        self.indent_level_up()
+        # 如果按钮有按下效果，添加自定义组件
+        if component.button_down_effect:
+            self.screen_ui_code.append(f"{self.indent_str}button_container:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}pressed_{component.button_down_effect} {component.button_down_effect_value}")
+        self.screen_ui_code.append(f"{self.indent_str}button:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'")
+        self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})")
+        self.screen_ui_code.append(f"{self.indent_str}background '{background}'")
+        self.screen_ui_code.append(f"{self.indent_str}text title")
+        self.screen_ui_code.append(f"{self.indent_str}action actions")
+        # 一些按钮特性
+        # focus_mask，对应FGUI中的点击“测试”。
+        if component.hit_test:
+            # 根据引用src查找image名
+            image_name = self.fgui_assets.get_componentname_by_id(component.hit_test.src)
+            self.screen_ui_code.append(f"{self.indent_str}focus_mask \'{image_name}\' pos {component.hit_test.pos}")
+
+        self.screen_ui_code.append("")
+
+        self.screen_code.extend(self.style_code)
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_ui_code)
+        self.renpy_code.extend(self.screen_code)
+
+    def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"):
+        return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5)
+
+    def generate_image_displayable(self, fgui_image):
+        """
+        生成图片组件。
+        前提为image对象的定义已经在generate_image_definitions中生成。
+        """
+        image_code = []
+        if not isinstance(fgui_image, FguiImage):
+            print("It is not a image displayable.")
+            return image_code
+
+        for image in self.fgui_assets.package_desc.image_list:
+            if fgui_image.src == image.id:
+                image_name = image.name
+                image_code.append(f"{self.indent_str}add \"{image_name}\":")
+                self.indent_level_up()
+                image_code.append(f"{self.indent_str}pos {fgui_image.xypos}")
+                # 必须指定，旋转和缩放都需要使用。
+                image_code.append(f"{self.indent_str}anchor {fgui_image.pivot}")
+                # FairyGUI中锚点固定为(0,0)或与轴心一致，轴心可指定为任意值。
+                # Ren'Py中旋转轴心固定为图片中心(0.5,0.5)或与锚点一致，锚点可指定为任意值。
+                # 若要与FairyGUI资源保持一致，需设置offset。
+                # size可能为None，需要获取
+                if not fgui_image.size:
+                    size = self.fgui_assets.get_image_size_by_id(fgui_image.src)
+                else:
+                    size = fgui_image.size
+                if not fgui_image.pivot_is_anchor:
+                    xoffset = int(fgui_image.pivot[0] * size[0])
+                    yoffset = int(fgui_image.pivot[1] * size[1])
+                    image_code.append(f"{self.indent_str}xoffset {xoffset}")
+                    image_code.append(f"{self.indent_str}yoffset {yoffset}")
+                else:
+                    image_code.append(f"{self.indent_str}transform_anchor {fgui_image.pivot_is_anchor}")
+
+                if fgui_image.rotation:
+                    image_code.append(f"{self.indent_str}rotate {fgui_image.rotation}")
+                if fgui_image.alpha != 1.0:
+                    image_code.append(f"{self.indent_str}alpha {fgui_image.alpha}")
+                if fgui_image.scale != (1.0, 1.0):
+                    image_code.append(f"{self.indent_str}xzoom {fgui_image.scale[0]} yzoom {fgui_image.scale[1]}")
+                # 九宫格或平铺图片需要指定尺寸
+                if image.scale:
+                    image_code.append(f"{self.indent_str}xysize {size}")
+                self.indent_level_down()
+                break
+        return image_code
+
+    def generate_graph_displayable(self, fgui_graph):
+        """
+        生成图形组件。图形组件有多种类别：
+        None: 空白
+        rect: 矩形(可带圆角)
+        eclipse: 椭圆(包括圆形)
+        regular_polygon: 正多边形
+        polygon: 多边形
+        """
+        graph_code = []
+
+        if not isinstance(fgui_graph, FguiGraph):
+            print("It is not a graph displayable.")
+            return graph_code
+
+        # 空白直接使用Null。后续可能存在一些relation相关不匹配。
+        if fgui_graph.type is None:
+            graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}")
+        # 矩形(圆边矩形)。原生组件不支持圆边矩形，使用自定义shader实现。
+        elif fgui_graph.type == "rect":
+            # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。
+            graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\
+                                .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\
+                                .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
+                                .replace('{image_size}', str(fgui_graph.size))\
+                                .replace('{round_radius}', str(fgui_graph.corner_radius))\
+                                .replace('{stroke_thickness}', str(fgui_graph.stroke_width))
+            # 直接在整个脚本对象中添加graph定义。
+            self.renpy_code.append(graph_img_def)
+        # 椭圆(圆形)。Ren'Py尚不支持椭圆，使用自定义shader实现。
+        elif fgui_graph.type == "eclipse":
+            graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\
+                                .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\
+                                .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
+                                .replace('{image_size}', str(fgui_graph.size))\
+                                .replace('{stroke_thickness}', str(fgui_graph.stroke_width))
+            # 直接在整个脚本对象中添加graph定义。
+            self.renpy_code.append(graph_img_def)
+
+        # 生成screen中的部分，带transform。
+        graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\":")
+        self.indent_level_up()
+        graph_code.append(f"{self.indent_str}xysize {fgui_graph.size}")
+        graph_code.append(f"{self.indent_str}pos {fgui_graph.xypos}")
+        graph_code.append(f"{self.indent_str}at transform:")
+        self.indent_level_up()
+        graph_code.append(f"{self.indent_str}anchor {fgui_graph.pivot}")
+        if not fgui_graph.pivot_is_anchor:
+            size = fgui_graph.size
+            xoffset = int(fgui_graph.pivot[0] * size[0])
+            yoffset = int(fgui_graph.pivot[1] * size[1])
+            graph_code.append(f"{self.indent_str}xoffset {xoffset}")
+            graph_code.append(f"{self.indent_str}yoffset {yoffset}")
+        graph_code.append(f"{self.indent_str}transform_anchor True")
+        if fgui_graph.rotation:
+            graph_code.append(f"{self.indent_str}rotate {fgui_graph.rotation}")
+        if fgui_graph.alpha != 1.0:
+            graph_code.append(f"{self.indent_str}alpha {fgui_graph.alpha}")
+        if fgui_graph.scale != (1.0, 1.0):
+            graph_code.append(f"{self.indent_str}xzoom {fgui_graph.scale[0]} yzoom {fgui_graph.scale[1]}")
+        self.indent_level_down()
+
+        self.indent_level_down()
+
+        return graph_code
+
+    def generate_text_displayable(self, fgui_text):
+        """
+        生成文本组件。非按钮的组件可能存在多个不同文本，不单独生成样式。
+        """
+        text_code = []
+
+        # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
+        if not isinstance(fgui_text, FguiText):
+            print("It is not a text displayable.")
+            return text_code
+
+        # 直接定义text组件。
+        # 处理换行符
+        text_str = fgui_text.text.replace("\n", "\\n").replace("\r", "\\n")
+        # 需要根据is_input区分文本组件与输入框
+        if fgui_text.is_input:
+            # Ren'Py中的直接使用input组件无法在多个输入框的情况下切换焦点，也无法点击空白区域让所有输入框失去焦点。
+            # 需要使用button作为父组件，与InputValue关联。整个界面添加一个dismiss，空白区域点击事件让输入框失去焦点。
+            # pass
+            # 添加InputValue变量。
+            self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name} = '{fgui_text.text}'")
+            self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name}_input_value = ScreenVariableInputValue('{fgui_text.name}', default=False)")
+            if self.screen_has_dismiss == False:
+                self.screen_has_dismiss = True
+                # 若prompt不为空，需要在screen中添加一个输入检测函数
+                if fgui_text.prompt:
+                    self.screen_function_code.append("    python:\n        def check_input_length(input_value_object):\n            str_length = len(input_value_object.get_text())\n            current, editable = renpy.get_editable_input_value()\n            return (not editable or current!=input_value_object) and str_length == 0\n")
+            self.dismiss_action_list.append(f"{fgui_text.name}_input_value.Disable()")
+            # 用按钮装载input
+            text_code.append(f"{self.indent_str}button:")
+            self.indent_level_up()
+            text_code.append(f"{self.indent_str}action {fgui_text.name}_input_value.Enable()")
+            self.indent_level_down()
+
+        else:
+            text_code.append(f"{self.indent_str}text \"{text_str}\":")
+        self.indent_level_up()
+        text_code.append(f"{self.indent_str}xysize {fgui_text.size}")
+        text_code.append(f"{self.indent_str}pos {fgui_text.xypos}")
+        text_code.append(f"{self.indent_str}at transform:")
+        self.indent_level_up()
+        text_code.append(f"{self.indent_str}anchor {fgui_text.pivot}")
+        if not fgui_text.pivot_is_anchor:
+            size = fgui_text.size
+            xoffset = int(fgui_text.pivot[0] * size[0])
+            yoffset = int(fgui_text.pivot[1] * size[1])
+            text_code.append(f"{self.indent_str}xoffset {xoffset}")
+            text_code.append(f"{self.indent_str}yoffset {yoffset}")
+        text_code.append(f"{self.indent_str}transform_anchor True")
+        if fgui_text.rotation:
+            text_code.append(f"{self.indent_str}rotate {fgui_text.rotation}")
+        if fgui_text.alpha != 1.0:
+            text_code.append(f"{self.indent_str}alpha {fgui_text.alpha}")
+        if fgui_text.scale != (1.0, 1.0):
+            text_code.append(f"{self.indent_str}xzoom {fgui_text.scale[0]} yzoom {fgui_text.scale[1]}")
+        self.indent_level_down()
+
+        # input组件额外内容
+        if fgui_text.is_input:
+            # 若有prompt，添加一个带显示条件的text。
+            if fgui_text.prompt:
+                prompt_str = fgui_text.prompt.replace("\n", "\\n").replace("\r", "\\n")
+                text_code.append(f"{self.indent_str}showif check_input_length({fgui_text.name}_input_value):")
+                self.indent_level_up()
+                text_code.append(f"{self.indent_str}text \"{prompt_str}\":")
+                self.indent_level_up()
+                # 暂时使用与input相同样式
+                text_font =  fgui_text.font if fgui_text.font else "SourceHanSansLite"
+                if not text_font in self.font_name_list:
+                    self.font_name_list.append(text_font)
+                text_code.append(f"{self.indent_str}font \"{text_font}\"")
+                text_code.append(f"{self.indent_str}size {fgui_text.font_size}")
+                text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"")
+                has_shadow = fgui_text.shadow_color
+                shadow_width = fgui_text.stroke_size
+                has_outline = fgui_text.stroke_color
+                if has_shadow:
+                    shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+                if has_outline:
+                    stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+                if has_shadow and not has_outline:
+                    text_code.append(f"{self.indent_str}outlines [{shadow_outline}]")
+                if not has_shadow and has_outline:
+                    text_code.append(f"{self.indent_str}outlines [{stroke_outline}]")
+                if has_shadow and has_outline:
+                    text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]")
+                if fgui_text.letter_spacing:
+                    text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}")
+                if fgui_text.leading:
+                    text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}")
+                xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
+                text_code.append(f"{self.indent_str}textalign {xalign}")
+                if fgui_text.bold:
+                    text_code.append(f"{self.indent_str}bold {fgui_text.bold}")
+                if fgui_text.italic:
+                    text_code.append(f"{self.indent_str}italic {fgui_text.italic}")
+                if fgui_text.underline:
+                    text_code.append(f"{self.indent_str}underline {fgui_text.underline}")
+                if fgui_text.strike:
+                    text_code.append(f"{self.indent_str}strikethrough {fgui_text.strike}")
+                self.indent_level_down(leveldown=2)
+
+
+            text_code.append(f"{self.indent_str}input:")
+            self.indent_level_up()
+            text_code.append(f"{self.indent_str}value {fgui_text.name}_input_value")
+
+        # 字体可能为空，改为Ren'Py内置默认字体SourceHanSansLite
+        text_font =  fgui_text.font if fgui_text.font else "SourceHanSansLite"
+        # 此处的字体名缺少后缀，在Ren'Py中直接使用会报错。
+        # 需将字体名添加到font_name_list中，并替换renpy_font_map_definition模板中对应内容。
+        if not text_font in self.font_name_list:
+            self.font_name_list.append(text_font)
+        text_code.append(f"{self.indent_str}font \"{text_font}\"")
+        text_code.append(f"{self.indent_str}size {fgui_text.font_size}")
+        text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"")
+        # Ren'Py中使用两层outlines分别实现投影和描边
+        # FGUI中的投影包含描边，投影的size等于描边的size，默认值为0；Ren'Py中允许outlines为0，依然有效果。
+        # Ren'Py中的property outlines是列表，先投影后描边。
+        has_shadow = fgui_text.shadow_color
+        shadow_width = fgui_text.stroke_size
+        has_outline = fgui_text.stroke_color
+        if has_shadow:
+            shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+        if has_outline:
+            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+        if has_shadow and not has_outline:
+            text_code.append(f"{self.indent_str}outlines [{shadow_outline}]")
+        if not has_shadow and has_outline:
+            text_code.append(f"{self.indent_str}outlines [{stroke_outline}]")
+        if has_shadow and has_outline:
+            text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]")
+        # 字间距与行距
+        if fgui_text.letter_spacing:
+            text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}")
+        if fgui_text.leading:
+            text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}")
+
+        # Ren'Py中只有文本宽度小于组件宽度的水平方向对齐设置
+        xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
+        text_code.append(f"{self.indent_str}textalign {xalign}")
+        # 粗体、斜体、下划线、删除线
+        if fgui_text.bold:
+            text_code.append(f"{self.indent_str}bold {fgui_text.bold}")
+        if fgui_text.italic:
+            text_code.append(f"{self.indent_str}italic {fgui_text.italic}")
+        if fgui_text.underline:
+            text_code.append(f"{self.indent_str}underline {fgui_text.underline}")
+        if fgui_text.strike:
+            text_code.append(f"{self.indent_str}strikethrough {fgui_text.strike}")
+        # 输入框特有properties
+        if fgui_text.is_input:
+            text_code.append(f"{self.indent_str}pixel_width {fgui_text.size[0]}")
+            if not fgui_text.single_line:
+                text_code.append(f"{self.indent_str}multiline True")
+            if fgui_text.max_length:
+                text_code.append(f"{self.indent_str}length {fgui_text.max_length}")
+            if fgui_text.is_password:
+                text_code.append(f"{self.indent_str}mask '*'")
+            # FGUI的输入限制使用正则表达式。在Ren'Py中使用字符串。此处仅为占位，无效果。
+            text_code.append(f"{self.indent_str}allow {{}}")
+            text_code.append(f"{self.indent_str}exclude {{}}")
+        if fgui_text.is_input:
+            self.indent_level_down()
+
+        self.indent_level_down()
+        return text_code
+
+    def generate_list_displayable(self, fgui_list):
+        """
+        生成列表。
+        """
+        list_code = []
+        if not isinstance(fgui_list, FguiList):
+            print("It is not a list displayable.")
+            return list_code
+
+        # 默认引用组件可能是图片或其他组件，后续处理方式不同。
+        default_item = self.fgui_assets.get_component_by_id(fgui_list.default_item_id)
+        default_item_type = None
+        default_item_name = None
+        item_number = len(fgui_list.item_list)
+        # 若为组件
+        if default_item:
+            default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id)
+            default_item_type = 'component'
+        # 若为图片
+        else:
+            default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id)
+            if default_item_name:
+                default_item_type = 'image'
+            else:
+                print("Ref com not found.")
+                return list_code
+
+        # 根据“溢出处理”是否可见区分处理。
+        # 若“可见”，则使用hbox、vbox和grid。
+        if fgui_list.overflow == "visible":
+            # 单列竖排，使用vbox
+            if fgui_list.layout == "column":
+                list_code.append(f"{self.indent_str}vbox:")
+            # 单行横排，使用hbox
+            elif fgui_list.layout == "row":
+                list_code.append(f"{self.indent_str}hbox:")
+            # 其他，使用grid
+            else:
+                list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
+
+        else:
+            list_code.append(f"{self.indent_str}vpgrid:")
+            self.indent_level_up()
+
+            # 若“隐藏”，使用不可滚动的vpgrid
+            if fgui_list.overflow == "hidden":
+                list_code.append(f"{self.indent_str}draggable False")
+                # 单列竖排
+                if fgui_list.layout == "column":
+                    cols = 1
+                    rows = item_number
+                # 单行横排
+                elif fgui_list.layout == "row":
+                    cols = item_number
+                    rows = 1
+                # 其他
+                else:
+                    cols = fgui_list.line_item_count
+                    rows = fgui_list.line_item_count2
+                list_code.append(f"{self.indent_str}cols {cols}")
+                list_code.append(f"{self.indent_str}rows {rows}")
+            # 若“滚动”，使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。
+            elif fgui_list.overflow == "scroll":
+                list_code.append(f"{self.indent_str}draggable True")
+                # 垂直滚动
+                if fgui_list.scroll == "vertical":
+                    pass
+                # 水平滚动
+                elif fgui_list.scroll == "horizontal":
+                    pass
+                # 自由滚动
+                elif fgui_list.scroll == "both":
+                    pass
+                # 单列竖排，使用vbox
+                if fgui_list.layout == "column":
+                    cols = 1
+                    rows = item_number
+                # 单行横排，使用hbox
+                elif fgui_list.layout == "row":
+                    cols = item_number
+                    rows = 1
+                # 其他，使用grid
+                else:
+                    cols = fgui_list.line_item_count
+                    rows = fgui_list.line_item_count2
+                list_code.append(f"{self.indent_str}cols {cols}")
+                list_code.append(f"{self.indent_str}rows {rows}")
+                if fgui_list.line_gap:
+                    list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
+                if fgui_list.col_gap:
+                    list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
+            self.indent_level_down()
+
+        self.indent_level_up()
+        list_code.append(f"{self.indent_str}pos {fgui_list.xypos}")
+        list_code.append(f"{self.indent_str}xysize {fgui_list.size}")
+        if fgui_list.margin:
+            list_code.append(f"{self.indent_str}margin {fgui_list.margin}")
+        # 添加元素
+        for item in fgui_list.item_list:
+            # 非默认元素
+            if item.item_url:
+                pass
+            # 默认元素
+            else:
+                if default_item_type == "image":
+                    list_code.append(f"{self.indent_str}add \'{default_item_name}\'")
+                elif default_item_type == "component":
+                    parameter_str = self.generate_button_parameter(item.item_title)
+                    list_code.append(f"{self.indent_str}use {default_item_name}({parameter_str})")
+
+        self.indent_level_down()
+        return list_code
+
+    def generate_renpy_code(self):
+        """生成完整的Ren'Py代码"""
+        self.renpy_code = []
+
+        # 添加文件头注释
+        self.renpy_code.append("# -*- coding: utf-8 -*-")
+        self.renpy_code.append("#")
+        self.renpy_code.append("# 从FairyGUI项目转换的Ren'Py界面代码")
+        self.renpy_code.append(f"# 资源包名: {self.fgui_assets.fgui_project_name}")
+        self.renpy_code.append("#")
+        self.renpy_code.append("")
+
+        # 生成图像定义
+        self.generate_image_definitions()
+
+        for component in self.fgui_assets.fgui_component_set:
+            if component.extention == 'Button':
+                self.generate_button_screen(component)
+            elif component.extention == 'ScrollBar':
+                pass
+            elif component.extention == 'Label':
+                pass
+            elif component.extention == 'Slider':
+                pass
+            elif component.extention == 'ComboBox':
+                pass
+            elif component.extention == 'ProgressBar':
+                pass
+            else:
+                self.generate_screen(component)
+                self.renpy_code.extend(self.screen_code)
+
+    def save_to_file(self, filename):
+        """
+        保存Ren'Py代码
+        """
+        with open(filename, 'w', encoding='utf-8') as f:
+            for line in self.renpy_code:
+                f.write(line + '\n')
+
+        print(f"Ren'Py代码已保存到: {filename}")
+
+    def from_templates_to_renpy(self, filename):
+        """
+        读取模板替换字符串并保存至Ren'Py目录
+        """
+        # 字体字典
+        with open(os.path.join(self.renpy_template_dir, self.font_map_template), 'r', encoding='utf-8') as file:
+            content = file.read()
+        font_name_list_str = ','.join(f'"{i}"' for i in self.font_name_list)
+        content = content.replace("{font_name_list}", font_name_list_str)
+
+        with open(filename, 'w', encoding='utf-8') as f:
+            f.write(content)
+
+    def get_graph_template(self, filename):
+        """
+        获取graph对应的image对象定义模板
+        """
+        with open(os.path.join(self.renpy_template_dir, filename), 'r', encoding='utf-8') as file:
+            content = file.read()
+        return content
+
+
+    def copy_predefine_files(self, source_dir, target_dir):
+        """
+        复制预定义cdd和cds的文件
+        """
+        print(f"source_dir: {source_dir}")
+        # 所有rpy文件
+        all_files = os.listdir(source_dir)
+        for file in all_files:
+            if file.endswith('.rpy'):
+                filename = os.path.basename(file)
+                source_file_path = os.path.join(source_dir, filename)
+                target_file_path = os.path.join(target_dir, filename)
+                shutil.copy2(source_file_path,target_file_path)
+
+
+    def copy_atlas_files(self, source_dir, target_dir):
+        """复制图集文件到目标目录，并将@替换为_"""
+
+        # 所有图集文件
+        atlas_files = self.fgui_assets.fgui_atlas_dicts.values()
+
+        # 复制文件并重命名
+        for atlas_file in atlas_files:
+            # 将@替换为_
+            new_filename = atlas_file.replace('@', '_')
+            source_path = os.path.join(source_dir, atlas_file)
+            target_path = os.path.join(target_dir, new_filename)
+            shutil.copy2(source_path, target_path)
+            print(f"✓ 复制并重命名图集文件: {atlas_file} -> {new_filename}")
+
+        return len(atlas_files)
+
+def convert(argv):
+    """
+    主函数：解析FguiDemoPackage并转换为Ren'Py代码
+    """
+    # 创建命令行参数解析器
+    parser = argparse.ArgumentParser(description='FairyGUI资源到Ren\'Py界面脚本的转换器',
+                                   epilog='使用示例:\n'
+                                          '  python Fgui2RenpyConverter.py -i "F:\\FguiDemoPackage" -o "F:\\RenpyProjects\\MyGame"\n'
+                                          '  python Fgui2RenpyConverter.py --input "MyFGUIAssetPackage" --output "/path/renpy/project"',
+                                   formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument('-i', '--input', type=str, 
+                        help='输入FairyGUI资源文件所在目录名 (目录中需存在同名 .bytes 文件)')
+    parser.add_argument('-o', '--output', type=str, 
+                        help='输出Ren\'Py项目基目录路径 (即Ren\'Py项目根目录)')
+    
+    # 解析命令行参数
+    args = parser.parse_args(argv[1:] if argv and len(argv) > 1 else [])
+    
+    # 检查必需的参数
+    if not args.input:
+        print("错误: 必须指定输入目录 (-i 或 --input)")
+        parser.print_help()
+        return
+    
+    if not args.output:
+        print("错误: 必须指定输出目录 (-o 或 --output)")
+        parser.print_help()
+        return
+    
+    fgui_project_path = args.input
+    if os.path.exists(fgui_project_path) and os.path.isdir(fgui_project_path):
+        fgui_project_name = os.path.basename(fgui_project_path)
+    else:
+        print(f"错误: 目录 {fgui_project_path} 不存在或不是有效目录")
+        return
+    
+    renpy_base_dir = args.output
+
+    print("开始将FairyGUI资源文件转换为Ren'Py脚本...")
+    print("=" * 50)
+
+    try:
+
+        # 检查文件是否存在
+        package_file = f"{fgui_project_path}/{fgui_project_name}.bytes"
+        sprite_file = f"{fgui_project_path}/{fgui_project_name}@sprites.bytes"
+
+        if not os.path.exists(package_file):
+            print(f"错误: 找不到文件 {package_file}")
+            return
+
+        if not os.path.exists(sprite_file):
+            print(f"错误: 找不到文件 {sprite_file}")
+            return
+
+        # 创建Ren'Py游戏基础目录结构
+        game_dir = os.path.join(renpy_base_dir, "game")
+        images_dir = os.path.join(game_dir, "images")
+        scripts_dir = os.path.join(game_dir, "scripts")
+
+        # 创建目录
+        os.makedirs(game_dir, exist_ok=True)
+        os.makedirs(images_dir, exist_ok=True)
+        os.makedirs(scripts_dir, exist_ok=True)
+        print(f"创建目录结构: {renpy_base_dir}/")
+        print(f"├── game/")
+        print(f"└── game/images/")
+        print(f"└── game/scripts/")
+
+        # 创建FguiAssets对象
+        print("\n正在解析FairyGUI资源...")
+        fgui_assets = FguiAssets(fgui_project_path)
+        print("FairyGUI资源解析完成")
+
+        # 创建转换器
+        print("正在创建转换器...")
+        converter = FguiToRenpyConverter(fgui_assets)
+        print("转换器创建完成")
+
+        # 生成Ren'Py代码
+        print("正在生成Ren'Py代码...")
+        converter.generate_renpy_code()
+        print("Ren'Py代码生成完成")
+
+        # 保存.rpy文件到game目录
+        output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy")
+        converter.save_to_file(output_file)
+
+        # 部分预定义模板文件修改参数并保存
+        font_map_definition_file = os.path.join(scripts_dir, "font_map.rpy")
+        converter.from_templates_to_renpy(font_map_definition_file)
+
+        # 复制预定义cdd和cds文件
+        converter.copy_predefine_files(converter.renpy_template_dir, scripts_dir)
+
+        # 复制图集文件到images目录
+        print("\n正在复制图集文件...")
+        current_dir = os.getcwd()
+        atlas_count = converter.copy_atlas_files(fgui_project_path, images_dir)
+        print(f"复制了 {atlas_count} 个图集文件")
+
+
+    except Exception as e:
+        print(f"❌ 转换过程中出现错误: {e}")
+        import traceback
+        traceback.print_exc()
+
+if __name__ == "__main__":
+    convert(sys.argv)
\ No newline at end of file
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy b/src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy
new file mode 100644
index 0000000..d2356a5
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/01_renpy_cdd.rpy
@@ -0,0 +1,175 @@
+python early:
+
+    import pygame
+    import math
+
+    class ButtonContainer(renpy.display.behavior.Button):
+        """
+        按钮容器类，按下后有缩放和改变颜色(未实现)效果。
+        """
+        def __init__(self, pressed_scale=1.0, pressed_dark=1.0, *args, **kwargs):
+            super(ButtonContainer, self).__init__(**kwargs)
+            self.pressed_scale = pressed_scale
+            # FGUI中变暗的取值范围为0～1，0完全黑，1完全无效果。(编辑器中允许输入值超过1，但无效果。)
+            # 此处使用BrightnessMatrix类，入参取值范围-1～1，-1完全变黑，0完全无效果，1完全变白。
+            # 因此需要做一个转换
+            self.pressed_dark = min(pressed_dark, 1.0) - 1.0
+            self.brightness_matrix = BrightnessMatrix(value=self.pressed_dark)
+            self.button_pressed = False
+            self.width = 0
+            self.height = 0
+            self.blit_pos = (0, 0)
+
+        def render(self, width, height, st, at):
+            if self.button_pressed and self.pressed_dark != 0:
+                t = Transform(child=self.child, anchor=(0.5, 0.5), matrixcolor=self.brightness_matrix)
+            else:
+                t = Transform(child=self.child, anchor=(0.5, 0.5), matrixcolor=None)
+            child_render = renpy.render(t, width, height, st, at)
+            self.width, self.height = child_render.get_size()
+            self.size = (self.width, self.height)
+            render = renpy.Render(self.width, self.height)
+            if self.button_pressed:
+                if self.pressed_scale != 1.0:
+                    child_render.zoom(self.pressed_scale, self.pressed_scale)
+                    # 为了居中，重新计算blit坐标
+                    self.blit_pos = ((int)(self.width*(1-self.pressed_scale)/2), (int)(self.height*(1-self.pressed_scale)/2))
+            else:
+                self.blit_pos = (0, 0)
+            render.blit(child_render, self.blit_pos)
+            return render
+
+        def event(self, ev, x, y, st):
+            if renpy.map_event(ev, "mousedown_1") and renpy.is_pixel_opaque(self.child, self.width, self.height, st=st, at=0, x=x, y=y) and not self.button_pressed:
+                self.button_pressed = True
+                renpy.redraw(self, 0)
+                return self.child.event(ev, x, y, st)
+            if self.button_pressed:
+                if renpy.map_event(ev, "mouseup_1"):
+                    self.button_pressed = False
+                    renpy.redraw(self, 0)
+                elif  ev.type == pygame.MOUSEMOTION and ev.buttons[0] != 1 :
+                    self.button_pressed = False
+                    renpy.redraw(self, 0)
+            return self.child.event(ev, x, y, st)
+
+        def visit(self):
+            return [ self.child ]
+
+python early:
+    renpy.register_sl_displayable("button_container", ButtonContainer, "pressed_button", 1)\
+        .add_property("pressed_scale")\
+        .add_property("pressed_dark")\
+        .add_property_group("button")
+    
+init python:
+    class SquenceAnimator(renpy.Displayable):
+        """
+        多图序列帧动画组件。
+        """
+        def __init__(self, prefix, separator, begin_index, end_index, interval, loop=True, **kwargs):
+            super(SquenceAnimator, self).__init__(**kwargs)
+            self.prefix = prefix
+            self.separator = separator
+            self.begin_index = begin_index
+            self.end_index = end_index
+            self.length = end_index - begin_index + 1
+
+
+            self.sequence = []
+            for i in range(begin_index, end_index+1):
+                self.sequence.append(renpy.displayable(self.prefix + self.separator + str(i)))
+
+            self.current_index = 0
+            self.show_timebase = 0
+
+            self.interval = interval
+            self.loop = loop
+
+        def render(self, width, height, st, at):
+            ## st为0时，表示组件重新显示
+            if st == 0:
+                self.show_timebase = 0
+                self.current_index = 0
+            if (st >= (self.show_timebase + self.interval)):
+                self.show_timebase = st
+                self.current_index += 1
+                if self.current_index >= self.length:
+                    if self.loop:
+                        self.current_index = 0
+                    else:
+                        self.current_index = self.length - 1
+                        
+            render = renpy.render(self.sequence[self.current_index], width, height, st, at)
+            renpy.redraw(self, 0)
+
+            return render
+
+        # 重置序列帧
+        def reset_sequence_index(self):
+            self.current_index = 0
+
+        def get_frame_image(self, index):
+            return self.sequence[index]
+
+    class SquenceAnimator2(renpy.Displayable):
+        """
+        单图序列帧动画组件。
+        """
+        def __init__(self, img, row, column, interval, loop=True, **kwargs):
+
+            super(SquenceAnimator2, self).__init__(**kwargs)
+            # im入参是字符串，需要转为Image对象，获取尺寸信息
+            self.img = Image(img)
+            self.size = renpy.image_size(self.img)
+            # 行数
+            self.row = row
+            # 列数
+            self.column = column
+            # 单帧宽度
+            self.frame_width = int(self.size[0] / column)
+            # 单帧高度
+            self.frame_height = int(self.size[1] / row)
+            # 序列帧长度
+            self.length = row * column
+
+            self.sequence = []
+            # 循环嵌套切割单帧图像
+            for i in range(row):
+                for j in range(column):
+                    # im.Crop()已被标记为deprecated，但剪裁边缘正确。
+                    # Crop()方法在右、低两边会有错误。
+                    # 参考 https://github.com/renpy/renpy/issues/6376
+                    self.sequence.append(im.Crop(self.img, (self.frame_width*j, self.frame_height*i, self.frame_width, self.frame_height)))
+
+            self.current_index = 0
+            self.show_timebase = 0
+
+            self.interval = interval
+            self.loop = loop
+
+        def render(self, width, height, st, at):
+            ## st为0时，表示组件重新显示
+            if st == 0:
+                self.show_timebase = 0
+                self.current_index = 0
+            if (st >= (self.show_timebase + self.interval)):
+                self.show_timebase = st
+                self.current_index += 1
+                if self.current_index >= self.length:
+                    if self.loop:
+                        self.current_index = 0
+                    else:
+                        self.current_index = self.length - 1
+
+            render = renpy.render(self.sequence[self.current_index], width, height, st, at)
+            renpy.redraw(self, 0)
+
+            return render
+
+        # 重置序列帧
+        def reset_sequence_index(self):
+            self.current_index = 0
+
+        def get_frame_image(self, index):
+            return self.sequence[index]
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
new file mode 100644
index 0000000..c14bf28
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
@@ -0,0 +1,99 @@
+init python:
+
+    renpy.register_shader("CursedOctopus.rectangle", variables="""
+        uniform vec4 u_rectangle_color;
+        uniform vec4 u_stroke_color;
+        uniform vec2 u_model_size;
+        uniform float u_radius;
+        uniform float u_thickness;
+        attribute vec2 a_tex_coord;
+        varying vec2 v_tex_coord;
+    """, vertex_300="""
+        v_tex_coord = a_tex_coord;
+    """,fragment_functions="""
+    float roundedBoxSDF(vec2 pos, vec2 border, float radius){
+        vec2 dis = abs(pos) - border + vec2(radius,radius);
+        return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - radius;
+    }
+    """,fragment_300="""
+        vec2 uv = v_tex_coord - vec2(0.5, 0.5);
+        vec2 tex_pos = uv * u_model_size;
+        float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius);
+        float border_alpha = (1.0 - step(0.0, out_distance)) * u_stroke_color.a;
+        float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius);
+        float fill_alpha = (1.0 - step(0.0, in_distance)) * u_rectangle_color.a;
+        vec4 c1 = step(1-fill_alpha, 0) * u_rectangle_color;
+        vec4 c2 = step(fill_alpha, 0) * border_alpha * u_stroke_color;
+        gl_FragColor = c1 + c2;
+    """)
+
+    renpy.register_shader("CursedOctopus.rectangleAA", variables="""
+        uniform vec4 u_rectangle_color;
+        uniform vec4 u_stroke_color;
+        uniform vec2 u_model_size;
+        uniform float u_radius;
+        uniform float u_thickness;
+        uniform float u_edge_softness;
+        attribute vec2 a_tex_coord;
+        varying vec2 v_tex_coord;
+    """, vertex_300="""
+        v_tex_coord = a_tex_coord;
+    """,fragment_functions="""
+    float roundedBoxSDF(vec2 pos, vec2 border, float radius){
+        vec2 dis = abs(pos) - border + vec2(radius,radius);
+        return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - radius;
+    }
+    """,fragment_300="""
+        vec2 uv = v_tex_coord - vec2(0.5, 0.5);
+        vec2 tex_pos = uv * u_model_size;
+        float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius);
+        float border_alpha = (1.0 - smoothstep(-u_edge_softness, u_edge_softness, out_distance)) * u_stroke_color.a;
+        float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius);
+        float fill_alpha = (1.0 - smoothstep(0, u_edge_softness, in_distance)) * u_rectangle_color.a;
+        vec4 c1 = fill_alpha * u_rectangle_color;
+        vec4 c2 = border_alpha * u_stroke_color;
+        gl_FragColor = mix(c2, c1, fill_alpha);
+    """)
+
+    renpy.register_shader("CursedOctopus.ellipse", variables="""
+        uniform vec4 u_ellipse_color;
+        uniform vec4 u_stroke_color;
+        uniform vec2 u_model_size;
+        uniform float u_thickness;
+        attribute vec2 a_tex_coord;
+        varying vec2 v_tex_coord;
+    """, vertex_300="""
+        v_tex_coord = a_tex_coord;
+    """,fragment_300="""
+        vec2 uv = v_tex_coord - vec2(0.5, 0.5);
+        float out_distance = length(uv);
+        float border_alpha = step(out_distance, 0.5);
+        vec2 tex_pos = uv * u_model_size;
+        float in_distance = length((abs(tex_pos+normalize(uv*u_thickness)*u_thickness))/u_model_size);
+        float fill_alpha = step(in_distance, 0.5);
+        vec4 c1 = step(1-fill_alpha, 0) * u_ellipse_color;
+        vec4 c2 = step(fill_alpha, 0) * border_alpha * u_stroke_color;
+        gl_FragColor = c1 + c2;
+    """)
+
+    renpy.register_shader("CursedOctopus.ellipseAA", variables="""
+        uniform vec4 u_ellipse_color;
+        uniform vec4 u_stroke_color;
+        uniform vec2 u_model_size;
+        uniform float u_thickness;
+        uniform float u_edge_softness;
+        attribute vec2 a_tex_coord;
+        varying vec2 v_tex_coord;
+    """, vertex_300="""
+        v_tex_coord = a_tex_coord;
+    """,fragment_300="""
+        vec2 uv = v_tex_coord - vec2(0.5, 0.5);
+        float out_distance = length(uv);
+        float border_alpha = smoothstep(out_distance-u_edge_softness, out_distance, 0.5-u_edge_softness);
+        vec2 tex_pos = uv * u_model_size;
+        float in_distance = length((abs(tex_pos+normalize(uv*u_thickness)*u_thickness))/u_model_size);
+        float fill_alpha = smoothstep(in_distance-u_edge_softness, in_distance, 0.5-u_edge_softness);
+        vec4 c1 = fill_alpha * u_ellipse_color;
+        vec4 c2 = border_alpha * u_stroke_color;
+        gl_FragColor = mix(c2, c1, fill_alpha);
+    """)
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
new file mode 100644
index 0000000..1f77827
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
@@ -0,0 +1,2 @@
+# 抗锯齿的椭圆(圆形)图像定义
+image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01)
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
new file mode 100644
index 0000000..8a36cb8
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
@@ -0,0 +1,2 @@
+# 椭圆(圆形)图像定义
+image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness})
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt
new file mode 100644
index 0000000..3255979
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt
@@ -0,0 +1,14 @@
+init python:
+    import os
+    font_name_list = [{font_name_list}]
+    font_file_ext = [".ttf", ".ttc", ".otf"]
+
+    for font_name in font_name_list:
+        for font_ext in font_file_ext:
+            font_file_name = f"{font_name}{font_ext}"
+            if os.path.exists(f"{config.gamedir}\\{font_file_name}") or os.path.exists(f"{config.gamedir}\\fonts\\{font_file_name}"):
+                config.font_name_map[font_name] = font_file_name
+                renpy.log(config.font_name_map)
+                break
+            else:
+                print(f"Could not Find font: {font_name}")
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
new file mode 100644
index 0000000..e54000c
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
@@ -0,0 +1,2 @@
+# 带光滑渐变的圆角矩形图像定义
+image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0)
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
new file mode 100644
index 0000000..9dab793
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
@@ -0,0 +1,2 @@
+# 圆角矩形图像定义
+image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness})
diff --git a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
index 2d91e18..bdacc97 100644
--- a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
+++ b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
@@ -9,6 +9,8 @@
 from ..translatablewidgetinterface import *
 from ..util.fileopen import FileOpenHelper
 
+from fgui_converter.utils.renpy.Fgui2RenpyConverter import *
+
 TR_gui_fguiconverter = TranslationDomain("gui_fguiconverter")
 
 class FguiConverterWidget(QWidget, ToolWidgetInterface):
@@ -18,7 +20,7 @@ class FguiConverterWidget(QWidget, ToolWidgetInterface):
   verifyCB : typing.Callable[[str], bool] | None
   fieldName : Translatable | str
   filter : Translatable | str
-  lastAddedPath : str
+  lastInputPath : str
   lastOutputPath : str
   ui : Ui_FguiConverterWidget
 
@@ -74,7 +76,7 @@ def __init__(self, parent : QWidget):
     self.verifyCB = None
     self.fieldName = ""
     self.filter = ""
-    self.lastAddedPath = ""
+    self.lastInputPath = ""
     self.lastOutputPath = ""
 
     self.bind_text(self.ui.fguiAssetDictLabel.setText, self._tr_fgui_assets_dict_label)
@@ -93,6 +95,7 @@ def __init__(self, parent : QWidget):
     self.setAcceptDrops(True)
     self.ui.outputDictButton.clicked.connect(self.itemOpenOutputDirectory)
     self.ui.outputDictLine.textChanged.connect(self.setOutputDict)
+    self.ui.convertButton.clicked.connect(self.generateRenpyUi)
 
   @classmethod
   def getToolInfo(cls, **kwargs) -> ToolWidgetInfo:
@@ -154,9 +157,9 @@ def addPath(self, path: str):
       if self.ui.listWidget.item(i).text() == path:
         return
     newItem = QListWidgetItem(path)
-    newItem.setToolTip(path)
+    #newItem.setToolTip(path)
     self.ui.listWidget.addItem(newItem)
-    self.lastAddedPath = path
+    self.lastInputPath = path
     self.listChanged.emit()
 
   @Slot()
@@ -193,7 +196,7 @@ def itemOpenContainingDirectory(self):
   @Slot()
   def itemAdd(self):
     dialogTitle = self._tr_select_dialog_title.format(field=str(self.fieldName))
-    initialDir = self.lastAddedPath if self.lastAddedPath else ""
+    initialDir = self.lastInputPath if self.lastInputPath else ""
     dialog = QFileDialog(self, dialogTitle, initialDir, str(self.filter))
     if self.isDirectoryMode:
       dialog.setFileMode(QFileDialog.Directory)
@@ -235,10 +238,21 @@ def setOutputDict(self, path: str):
     self.ui.outputDictLine.setText(path)
     self.lastOutputPath = path
 
-  @classmethod
-  def getToolInfo(cls, **kwargs) -> ToolWidgetInfo:
-    return ToolWidgetInfo(
-      idstr="guiconverter",
-      name=MainWindowInterface.tr_toolname_guiconverter,
-      widget=cls,
-    )
+  @Slot()
+  def generateRenpyUi(self):
+    # 检查当前输入输出设置。
+    # 报错暂时只在Python命令行打印，后续改为弹窗信息。
+    if self.ui.listWidget.count() > 0:
+      inputPathList = self.getCurrentList()
+    else:
+      print("Input Path List is Empty.")
+      return
+    outputPathStr = self.ui.outputDictLine.text()
+    if os.path.isdir(outputPathStr):
+      print(outputPathStr)
+    else:
+      print("Ren'Py Project base dictionary does not exsit.")
+      return
+    
+    # 创建FguiToRenpyConverter对象
+    convert(["Test", "-i", inputPathList[0], "-o", outputPathStr])
\ No newline at end of file

From 73b3cc5d50d277e190c4adbcc04c434e2a81a737 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 17 Oct 2025 10:11:27 +0800
Subject: [PATCH 03/40] Add independency in setup.cfg

---
 .gitignore | 2 ++
 setup.cfg  | 1 +
 2 files changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
index e642b8f..dfe4d74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -174,3 +174,5 @@ src/preppipe_gui_pyside6/forms/generated
 # version files for pyinstaller build
 versionfile_*.txt
 
+run_cli.py
+run_gui.py
diff --git a/setup.cfg b/setup.cfg
index ffe922b..8db1325 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,6 +42,7 @@ install_requires =
   xlsxwriter
   psd-tools
   antlr4-python3-runtime >= 4.10, < 4.11.0
+  lxml>=4.9.0
 
 # GUI extra dependencies here
 # https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#configuring-setup-using-setup-cfg-files

From 02a7c36df024811268f72d2ebe324704b3878103 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 17 Oct 2025 10:47:33 +0800
Subject: [PATCH 04/40] Modify README.md, add `-o` option to `pyside6-uic`
 command.

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index d2e99bf..fdff3c0 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ git config --local include.path $PWD/gitconfig
 
 需要手动执行的有：（请在上述开发环境配置完毕后执行）
 * 资源文件处理。请在获取[资源仓](#素材资源-assets)后，在仓库根目录下运行 `python3 ./build_assets.py` 以生成 `src/preppipe/assets/_install` 下的内容。该操作需要在资源列表更新时或任意资源类型保存的的内部数据结构改变时重新进行。
-* GUI 中 PySide6 `.ui` 文件编译。请在 `src/preppipe_gui_pyside6/forms` 目录下将所有诸如 `xxx.ui` 的文件使用命令 `pyside6-uic xxx.ui generated/ui_xxx.py` 编译成 `.py`。如果您使用 Linux，您可以直接用该目录下的 `Makefile`。该操作需要在任意 .ui 文件更改后重新执行。
+* GUI 中 PySide6 `.ui` 文件编译。请在 `src/preppipe_gui_pyside6/forms` 目录下将所有诸如 `xxx.ui` 的文件使用命令 `pyside6-uic xxx.ui -o generated/ui_xxx.py` 编译成 `.py`。如果您使用 Linux，您可以直接用该目录下的 `Makefile`。该操作需要在任意 .ui 文件更改后重新执行。
 
 ## GUI启动
 

From 0f867cea79350c02c18a0040505899c9f1445870 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sat, 18 Oct 2025 17:05:37 +0800
Subject: [PATCH 05/40] Add null "input/output" popup and fix some Converter
 bug

1. PrepPipe-gui support specifying item within multi input packages.
2. Popup message box if input/output is empty.
3. Converter Module add or fix delete or clean methods.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  7 ++++
 .../utils/renpy/Fgui2RenpyConverter.py        | 32 +++++++++++++++++++
 .../toolwidgets/fguiconverter.py              | 27 ++++++++++++++--
 3 files changed, 64 insertions(+), 2 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index a5af312..9d28fbb 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -997,10 +997,14 @@ def __init__(self, fgui_project_path):
 
     def clear(self):
         self.fgui_project_name = ''
+        self.package_desc_file = ''
+        self.sprite_desc_file = ''
         self.package_desc.clear()
         self.object_dict.clear()
         self.fgui_atlas_dicts.clear()
         self.fgui_component_set.clear()
+        self.fgui_image_set.clear()
+        self.fgui_atlas_dicts.clear()
 
     def get_componentname_by_id(self, id):
         return self.package_desc.id_name_mapping[id]
@@ -1015,3 +1019,6 @@ def get_image_size_by_id(self, id):
         for image in self.fgui_image_set:
             if image.image_id == id:
                 return (image.width, image.height)
+
+    def __del__(self):
+        self.clear()
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 0508eff..c1c60d3 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -855,6 +855,35 @@ def get_graph_template(self, filename):
             content = file.read()
         return content
 
+    def cleanup(self):
+        """
+        清理转换器资源
+        清理内存中的数据结构、模板缓存等资源
+        """
+        try:
+            self.fgui_assets = None
+            # 清理代码生成相关的列表
+            self.renpy_code.clear()
+            self.screen_code.clear()
+            self.screen_definition_head.clear()
+            self.screen_variable_code.clear()
+            self.screen_function_code.clear()
+            self.screen_ui_code.clear()
+            self.style_code.clear()
+            self.dismiss_action_list.clear()
+            
+            # 重置缩进相关状态
+            self.root_indent_level = 0
+            self.indent_str = ''
+            self.screen_has_dismiss = False
+            
+        except Exception as e:
+            print(f"清理资源时出现错误: {e}")
+
+    def __del__(self):
+        self.cleanup()
+
+
 
     def copy_predefine_files(self, source_dir, target_dir):
         """
@@ -989,6 +1018,9 @@ def convert(argv):
         atlas_count = converter.copy_atlas_files(fgui_project_path, images_dir)
         print(f"复制了 {atlas_count} 个图集文件")
 
+        # 一些清理
+        fgui_assets.clear()
+        converter.cleanup()
 
     except Exception as e:
         print(f"❌ 转换过程中出现错误: {e}")
diff --git a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
index bdacc97..4d866df 100644
--- a/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
+++ b/src/preppipe_gui_pyside6/toolwidgets/fguiconverter.py
@@ -238,6 +238,24 @@ def setOutputDict(self, path: str):
     self.ui.outputDictLine.setText(path)
     self.lastOutputPath = path
 
+  _tr_unable_to_transform = TR_gui_fguiassetsdictwidget.tr("unable_to_transform",
+    en="Unable to transform",
+    zh_cn="无法转换",
+    zh_hk="無法轉換",
+  )
+
+  _tr_input_required = TR_gui_fguiassetsdictwidget.tr("input_required",
+    en="Please specify input directory first",
+    zh_cn="请先指定输入文件夹",
+    zh_hk="請先指定輸入文件夾",
+  )
+
+  _tr_output_required = TR_gui_fguiassetsdictwidget.tr("output_required",
+    en="Please specify output directory first",
+    zh_cn="请先指定输出文件夹",
+    zh_hk="請先指定輸出文件夾",
+  )
+
   @Slot()
   def generateRenpyUi(self):
     # 检查当前输入输出设置。
@@ -246,13 +264,18 @@ def generateRenpyUi(self):
       inputPathList = self.getCurrentList()
     else:
       print("Input Path List is Empty.")
+      QMessageBox.critical(self, self._tr_unable_to_transform.get(), self._tr_input_required.get())
       return
     outputPathStr = self.ui.outputDictLine.text()
     if os.path.isdir(outputPathStr):
       print(outputPathStr)
     else:
       print("Ren'Py Project base dictionary does not exsit.")
+      QMessageBox.critical(self, self._tr_unable_to_transform.get(), self._tr_output_required.get())
       return
     
-    # 创建FguiToRenpyConverter对象
-    convert(["Test", "-i", inputPathList[0], "-o", outputPathStr])
\ No newline at end of file
+    curRow = self.ui.listWidget.currentRow()
+    if curRow >= 0:
+      #item = self.ui.listWidget.takeItem(curRow)
+      # 创建FguiToRenpyConverter对象
+      convert(["Test", "-i", inputPathList[curRow], "-o", outputPathStr])
\ No newline at end of file

From 743a764be9e4dfe64e6ab55164af0cb38acfb973 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 4 Nov 2025 18:30:19 +0800
Subject: [PATCH 06/40] Restructured button converter method

1. Restructuring of button converter method.
2. Update graph templates.
---
 .../utils/renpy/Fgui2RenpyConverter.py        | 337 +++++++++++++++---
 .../renpy_ellipseAA_template.txt              |  13 +-
 .../renpy_ellipse_template.txt                |  13 +-
 .../renpy_templates/renpy_null_template.txt   |   2 +
 .../renpy_rectangleAA_template.txt            |  13 +-
 .../renpy_rectangle_template.txt              |  13 +-
 6 files changed, 331 insertions(+), 60 deletions(-)
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index c1c60d3..72024c0 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -7,6 +7,15 @@
 import argparse
 
 import shutil
+from enum import IntEnum
+
+class DisplayableChildType(IntEnum):
+    NULL = 0
+    IMAGE = 1
+    GRAPH = 2
+    TEXT = 3
+    COMPONENT = 4
+    OTHER = 5
 
 # 添加当前目录到Python路径，以便导入FguiAssetsParseLib
 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
@@ -55,6 +64,7 @@ def __init__(self, fgui_assets):
                              os.path.join(os.path.dirname(os.path.abspath(__file__)), "renpy_templates"))
         self.font_map_template = 'renpy_font_map_definition.txt'
         self.graph_template_dict = {}
+        self.graph_template_dict['null'] = self.get_graph_template('renpy_null_template.txt')
         self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt')
         self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt')
 
@@ -130,6 +140,79 @@ def generate_image_definitions(self):
         image_definitions.append("")
         self.renpy_code.extend(image_definitions)
 
+    def generate_graph_definitions(self, fgui_graph):
+        """
+        生成图形组件定义。图形组件有多种类别：
+        None: 空白
+        rect: 矩形(可带圆角)
+        eclipse: 椭圆(包括圆形)
+        regular_polygon: 正多边形
+        polygon: 多边形
+        """
+        graph_code = []
+        graph_img_def = ''
+
+        if not isinstance(fgui_graph, FguiGraph):
+            print("It is not a graph displayable.")
+            return graph_code
+
+        xoffset = 0
+        yoffset = 0
+        if not fgui_graph.pivot_is_anchor:
+            size = fgui_graph.size
+            xoffset = int(fgui_graph.pivot[0] * size[0])
+            yoffset = int(fgui_graph.pivot[1] * size[1])
+
+        # 空白使用Null。
+        if fgui_graph.type is None:
+            # graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}")
+            self.graph_template_dict['null'].replace('{image_name}', fgui_graph.id)\
+                                .replace('{width}', str(fgui_graph.size[0]))\
+                                .replace('{height}', str(fgui_graph.size[1]))
+        # 矩形(圆边矩形)。原生组件不支持圆边矩形，使用自定义shader实现。
+        elif fgui_graph.type == "rect":
+            # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。
+            graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\
+                                .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\
+                                .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
+                                .replace('{image_size}', str(fgui_graph.size))\
+                                .replace('{round_radius}', str(fgui_graph.corner_radius))\
+                                .replace('{stroke_thickness}', str(fgui_graph.stroke_width))\
+                                .replace('{xysize}', str(fgui_graph.size))\
+                                .replace('{pos}', str(fgui_graph.xypos))\
+                                .replace('{anchor}', str(fgui_graph.pivot))\
+                                .replace('{xoffset}', str(xoffset))\
+                                .replace('{yoffset}', str(yoffset))\
+                                .replace('{transform_anchor}', str(True)) \
+                                .replace('{rotate}', str(fgui_graph.rotation))\
+                                .replace('{alpha}', str(fgui_graph.alpha))\
+                                .replace('{xzoom}', str(fgui_graph.scale[0]))\
+                                .replace('{yzoom}', str(fgui_graph.scale[1]))
+        # 椭圆(圆形)。Ren'Py尚不支持椭圆，使用自定义shader实现。
+        elif fgui_graph.type == "eclipse":
+            graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\
+                                .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\
+                                .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
+                                .replace('{image_size}', str(fgui_graph.size))\
+                                .replace('{stroke_thickness}', str(fgui_graph.stroke_width))\
+                                .replace('{xysize}', str(fgui_graph.size))\
+                                .replace('{pos}', str(fgui_graph.xypos))\
+                                .replace('{anchor}', str(fgui_graph.pivot))\
+                                .replace('{xoffset}', str(xoffset))\
+                                .replace('{yoffset}', str(yoffset))\
+                                .replace('{transform_anchor}', str(True)) \
+                                .replace('{rotate}', str(fgui_graph.rotation))\
+                                .replace('{alpha}', str(fgui_graph.alpha))\
+                                .replace('{xzoom}', str(fgui_graph.scale[0]))\
+                                .replace('{yzoom}', str(fgui_graph.scale[1]))
+        elif fgui_graph.type == "regular_polygon":
+            print("regular_polygon not implemented.")
+            pass
+        # 在整个脚本对象中添加graph定义。
+        graph_code.append(graph_img_def)
+
+        return graph_code
+
     def generate_screen(self, component):
         """
         生成screen定义。目标样例：
@@ -199,6 +282,7 @@ def generate_screen(self, component):
                     self.screen_ui_code.append(f"{self.indent_str}fixed:")
                     self.indent_level_up()
                     self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
                     parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data)
                     self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id \"{displayable.id}\"")
                     self.indent_level_down()
@@ -298,6 +382,76 @@ def generate_text_style(self, fgui_text, style_name):
             self.style_code.append(f"{style_indent}strikethrough {fgui_text.strike}")
         self.style_code.append("")
 
+    @staticmethod
+    def get_child_type(displayable):
+        if isinstance(displayable, FguiImage):
+            return DisplayableChildType.IMAGE
+        elif isinstance(displayable, FguiText):
+            return DisplayableChildType.TEXT
+        elif isinstance(displayable, FguiGraph):
+            return DisplayableChildType.GRAPH
+        elif isinstance(displayable, FguiComponent):
+            return DisplayableChildType.COMPONENT
+        else:
+            return DisplayableChildType.OTHER
+
+    def generate_button_children(self, fgui_button):
+        """
+        生成按钮各状态的子组件。
+        """
+
+        # 4种状态的子组件列表
+        idle_child_list = []
+        hover_child_list = []
+        selected_child_list = []
+        selected_hover_child_list = []
+        # 非激活状体子组件列表
+        insensitive_child_list = []
+        # 始终显示的子组件列表
+        always_show_child_list = []
+        # 5种类型的子组件字典
+        state_children_dict = {
+            'idle': idle_child_list,
+            'hover': hover_child_list,
+            'selected': selected_child_list,
+            'selected_hover': selected_hover_child_list,
+            'insensitive': insensitive_child_list,
+            'always_show': always_show_child_list
+        }
+        state_index_name_dict = {
+            0: 'idle',
+            1: 'hover',
+            2: 'selected',
+            3: 'selected_hover',
+            4: 'insensitive',
+            None: 'always_show'
+        }
+        # 默认按钮控制器为button，并且必定有4种状态，顺序分别为idle、hover、selected、selected_hover。
+        # 检查按钮控制器名称
+        if fgui_button.controller_list[0].name != 'button':
+            print("按钮控制器名不是button。")
+            return state_children_dict
+        # 检查按钮控制器的状态列表
+        state_list = fgui_button.controller_list[0].page_index_dict.keys()
+        state_number = min(len(state_list), 5) #暂时不处理5种以上的控制器状态
+        if state_number < 4:
+            print("按钮控制器状态总数小于4。")
+            return state_children_dict
+
+        # 将displayable_list中的子组件按状态分别添加到对应列表中
+        for displayable in fgui_button.display_list.displayable_list:
+            if displayable.gear_display is None:
+                state_children_dict['always_show'].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable)))
+                break
+            for i in range(0, state_number):
+                if displayable.gear_display and str(i) in displayable.gear_display.controller_index:
+                    state_children_dict[state_index_name_dict[i]].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable)))
+                    continue
+        # print(state_children_dict)
+
+        return state_children_dict
+
+
     def generate_button_screen(self, component):
 
         """
@@ -330,33 +484,93 @@ def generate_button_screen(self, component):
         self.dismiss_action_list.clear()
         self.style_code.clear()
 
+        # 4种状态的子组件列表
+        idle_child_list = []
+        hover_child_list = []
+        selected_child_list = []
+        selected_hover_child_list = []
+        # 非激活状体子组件列表
+        insensitive_child_list = []
+        # 始终显示的子组件列表
+        always_show_child_list = []
+        # 5种类型的子组件字典
+        state_children_dict = {
+            'idle': idle_child_list,
+            'selected': selected_child_list,
+            'hover': hover_child_list,
+            'selected_hover': selected_hover_child_list,
+            'insensitive': insensitive_child_list,
+            'always_show': always_show_child_list
+        }
+
+        state_index_name_dict = {
+            0: 'idle',
+            1: 'selected',
+            2: 'hover',
+            3: 'selected_hover',
+            4: 'insensitive',
+            None: 'always_show'
+        }
+        
+        # 图片id与name映射关系
+        image_id_name_mapping = {}
+
         if component.extention != 'Button':
             print("组件类型不是按钮")
             return
 
+        # 默认按钮控制器为button，并且必定有4种状态，顺序分别为idle、hover、selected、selected_hover。
+        # 可扩展为5种，第5种必需为insensitive，表示按钮不激活状态。
+        if component.controller_list[0].name != 'button':
+            print("按钮控制器名不是button。")
+            return
+        # 检查按钮控制器的状态列表
+        state_list = component.controller_list[0].page_index_dict.keys()
+        state_number = min(len(state_list), 5) #暂时不处理5种以上的控制器状态
+        if state_number < 4:
+            print("按钮控制器状态总数小于4。")
+            return
+
         # 生成按钮组件screen
         self.screen_definition_head.append("# 按钮组件Screen")
         self.screen_definition_head.append(f"# 从FairyGUI按钮组件{component.name}转换而来")
-        # screen_code.append("")
 
         id = component.id
         button_name = component.name
         xysize = component.size
         background = self.default_background
+
         default_title = ''
+        # state_children_dict = self.generate_button_children(component)
         for displayable in component.display_list.displayable_list:
+            # 不带显示控制器表示始终显示
+            if displayable.gear_display is None:
+                # state_children_dict['always_show'].append((displayable, FguiToRenpyConverter.get_child_type(displayable)))
+                # 加入到所有状态显示列表中
+                if displayable.name != 'title':
+                    for state_list in state_children_dict.values():
+                        state_list.append((displayable, FguiToRenpyConverter.get_child_type(displayable)))
+                else:
+                    pass
+            # 其他状态根据枚举值加入各列表
+            else:
+                for i in range(0, state_number):
+                    if str(i) in displayable.gear_display.controller_index:
+                        state_children_dict[state_index_name_dict[i]].append((displayable, FguiToRenpyConverter.get_child_type(displayable)))
+                        continue
             # 图片组件
             if isinstance(displayable, FguiImage):
                 for image in self.fgui_assets.package_desc.image_list:
                     if displayable.src == image.id:
-                        background = image.name
+                        image_id_name_mapping[displayable.id] = image.name
                         break
-                    # TODO
-                    # 处理不同图片分别用于idle、hover、selected_idle和selected_hover的情况
-            # 文本组件
+                # print(image_id_name_mapping)
+            elif isinstance(displayable, FguiGraph):
+                self.renpy_code.extend(self.generate_graph_definitions(displayable))
+                # break
+            # 文本组件。只处理名为title的文本组件。其他的文本待后续增加。
             # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
-            # if isinstance(displayable, FguiText) and displayable.name == 'title':
-            if isinstance(displayable, FguiText):
+            elif isinstance(displayable, FguiText) and displayable.name == 'title':
                 # 重置缩进级别
                 self.reset_indent_level()
                 default_title = displayable.text
@@ -364,6 +578,19 @@ def generate_button_screen(self, component):
                 self.generate_text_style(displayable, f"{button_name}_text")
                 # self.screen_code.extend(self.style_code)
 
+        # 根据state_children_dict生成各种对应状态的background
+        # idle_background
+        self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'])
+        # selected_background
+        self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'])
+        # hover_background
+        self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'],)
+        # selected_hover_background
+        self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'])
+        # insensitive_background
+        self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'])
+
+
         # 重置缩进级别
         self.reset_indent_level()
         self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions=NullAction()):")
@@ -377,7 +604,17 @@ def generate_button_screen(self, component):
         self.indent_level_up()
         self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'")
         self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})")
-        self.screen_ui_code.append(f"{self.indent_str}background '{background}'")
+        # self.screen_ui_code.append(f"{self.indent_str}background '{background}'")
+        if state_children_dict['idle']:
+            self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'")
+        if state_children_dict['selected']:
+            self.screen_ui_code.append(f"{self.indent_str}selected_background '{button_name}_selected_background'")
+        if state_children_dict['hover']:
+            self.screen_ui_code.append(f"{self.indent_str}hover_background '{button_name}_hover_background'")
+        if state_children_dict['selected_hover']:
+            self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'")
+        if state_children_dict['insensitive']:
+            self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'")
         self.screen_ui_code.append(f"{self.indent_str}text title")
         self.screen_ui_code.append(f"{self.indent_str}action actions")
         # 一些按钮特性
@@ -397,6 +634,40 @@ def generate_button_screen(self, component):
     def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"):
         return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5)
 
+    def generate_image_object(self, image_name, displayable_list):
+        """
+        生成image对象，入参为一些子组件列表，自带transform。
+        暂时不考虑允许列表中的子组件添加额外transform，只单纯堆叠。
+        """
+        if len(displayable_list) <= 0:
+            return
+        image_definitions = []
+        image_definitions.append("# 图像定义")
+        image_definitions.append("# 合成的图像对象")
+        indent_level = self.root_indent_level
+        self.reset_indent_level()
+        image_definitions.append(f"image {image_name}:")
+        self.indent_level_up()
+        for displayable, displayalbe_type in displayable_list:
+            image_definitions.append(f"{self.indent_str}contains:")
+            self.indent_level_up()
+            if displayalbe_type == DisplayableChildType.IMAGE:
+                name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                image_definitions.append(f"{self.indent_str}'{name}'")
+            elif displayalbe_type == DisplayableChildType.GRAPH:
+                image_definitions.append(f"{self.indent_str}'{displayable.id}'")
+            elif displayalbe_type == DisplayableChildType.TEXT:
+                image_definitions.append(f"{self.indent_str}Text({displayable.text})")
+            # 其他类型暂时用空对象占位
+            else:
+                image_definitions.append(f"{self.indent_str}Null()")
+            self.indent_level_down()
+
+        image_definitions.append("")
+
+        self.renpy_code.extend(image_definitions)
+        self.root_indent_level = indent_level
+
     def generate_image_displayable(self, fgui_image):
         """
         生成图片组件。
@@ -459,54 +730,8 @@ def generate_graph_displayable(self, fgui_graph):
             print("It is not a graph displayable.")
             return graph_code
 
-        # 空白直接使用Null。后续可能存在一些relation相关不匹配。
-        if fgui_graph.type is None:
-            graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}")
-        # 矩形(圆边矩形)。原生组件不支持圆边矩形，使用自定义shader实现。
-        elif fgui_graph.type == "rect":
-            # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。
-            graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\
-                                .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\
-                                .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
-                                .replace('{image_size}', str(fgui_graph.size))\
-                                .replace('{round_radius}', str(fgui_graph.corner_radius))\
-                                .replace('{stroke_thickness}', str(fgui_graph.stroke_width))
-            # 直接在整个脚本对象中添加graph定义。
-            self.renpy_code.append(graph_img_def)
-        # 椭圆(圆形)。Ren'Py尚不支持椭圆，使用自定义shader实现。
-        elif fgui_graph.type == "eclipse":
-            graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\
-                                .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\
-                                .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
-                                .replace('{image_size}', str(fgui_graph.size))\
-                                .replace('{stroke_thickness}', str(fgui_graph.stroke_width))
-            # 直接在整个脚本对象中添加graph定义。
-            self.renpy_code.append(graph_img_def)
-
-        # 生成screen中的部分，带transform。
-        graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\":")
-        self.indent_level_up()
-        graph_code.append(f"{self.indent_str}xysize {fgui_graph.size}")
-        graph_code.append(f"{self.indent_str}pos {fgui_graph.xypos}")
-        graph_code.append(f"{self.indent_str}at transform:")
-        self.indent_level_up()
-        graph_code.append(f"{self.indent_str}anchor {fgui_graph.pivot}")
-        if not fgui_graph.pivot_is_anchor:
-            size = fgui_graph.size
-            xoffset = int(fgui_graph.pivot[0] * size[0])
-            yoffset = int(fgui_graph.pivot[1] * size[1])
-            graph_code.append(f"{self.indent_str}xoffset {xoffset}")
-            graph_code.append(f"{self.indent_str}yoffset {yoffset}")
-        graph_code.append(f"{self.indent_str}transform_anchor True")
-        if fgui_graph.rotation:
-            graph_code.append(f"{self.indent_str}rotate {fgui_graph.rotation}")
-        if fgui_graph.alpha != 1.0:
-            graph_code.append(f"{self.indent_str}alpha {fgui_graph.alpha}")
-        if fgui_graph.scale != (1.0, 1.0):
-            graph_code.append(f"{self.indent_str}xzoom {fgui_graph.scale[0]} yzoom {fgui_graph.scale[1]}")
-        self.indent_level_down()
-
-        self.indent_level_down()
+        self.renpy_code.extend(self.generate_graph_definitions(fgui_graph))
+        graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\"")
 
         return graph_code
 
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
index 1f77827..cd6a884 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
@@ -1,2 +1,13 @@
 # 抗锯齿的椭圆(圆形)图像定义
-image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01)
+image {image_name}:
+    Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01)
+    xysize {xysize}
+    pos {pos}
+    anchor {anchor}
+    xoffset {xoffset}
+    yoffset {yoffset}
+    transform_anchor {transform_anchor}
+    rotate {rotate}
+    alpha {alpha}
+    xzoom {xzoom}
+    yzoom {yzoom}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
index 8a36cb8..364e171 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
@@ -1,2 +1,13 @@
 # 椭圆(圆形)图像定义
-image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness})
+image {image_name}:
+    Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness})
+    xysize {xysize}
+    pos {pos}
+    anchor {anchor}
+    xoffset {xoffset}
+    yoffset {yoffset}
+    transform_anchor {transform_anchor}
+    rotate {rotate}
+    alpha {alpha}
+    xzoom {xzoom}
+    yzoom {yzoom}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt
new file mode 100644
index 0000000..4bc008d
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_null_template.txt
@@ -0,0 +1,2 @@
+image {image_name}:
+    Null(width={width}, height={height})
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
index e54000c..75237be 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
@@ -1,2 +1,13 @@
 # 带光滑渐变的圆角矩形图像定义
-image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0)
+image {image_name}:
+    Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0)
+    xysize {xysize}
+    pos {pos}
+    anchor {anchor}
+    xoffset {xoffset}
+    yoffset {yoffset}
+    transform_anchor {transform_anchor}
+    rotate {rotate}
+    alpha {alpha}
+    xzoom {xzoom}
+    yzoom {yzoom}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
index 9dab793..eb60024 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
@@ -1,2 +1,13 @@
 # 圆角矩形图像定义
-image {image_name} = Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness})
+image {image_name}:
+    Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness})
+    xysize {xysize}
+    pos {pos}
+    anchor {anchor}
+    xoffset {xoffset}
+    yoffset {yoffset}
+    transform_anchor {transform_anchor}
+    rotate {rotate}
+    alpha {alpha}
+    xzoom {xzoom}
+    yzoom {yzoom}

From ac12c0e87f4c9142c8adf73e7a50aa42838dd775 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 4 Nov 2025 23:42:53 +0800
Subject: [PATCH 07/40] Add text style within Button

1. No "title" text within Buttons, add styles.
2. Fixed some problems of FguiAssetsParseLib Module.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  5 +-
 .../utils/renpy/Fgui2RenpyConverter.py        | 78 ++++++++++++++-----
 2 files changed, 62 insertions(+), 21 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 9d28fbb..34e18cb 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -750,9 +750,9 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.item_url = None
         if package_description_id:
             self.package_description_id = package_description_id
-            get_item_id(package_description_id)
+            self.get_item_id(package_description_id)
 
-    def get_item_id(packageDescription_id):
+    def get_item_id(self, packageDescription_id):
         self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):]
 
 class FguiGearBase:
@@ -868,6 +868,7 @@ def __init__(self, gear_item_tree):
             raise ValueError("xml tag is not gearSize.")
         super().__init__(gear_item_tree)
         self.index_value_dict = {} # 该字典存放控制器索引与对应尺寸
+        index_values = self.values
         if self.values:
             for i in range(len(self.values)):
                 item = index_values[i].split(",")
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 72024c0..dfa6605 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -284,7 +284,7 @@ def generate_screen(self, component):
                     self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
                     parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data)
-                    self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id \"{displayable.id}\"")
+                    self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{displayable.id}'")
                     self.indent_level_down()
 
         self.screen_code.extend(self.screen_definition_head)
@@ -348,9 +348,9 @@ def generate_text_style(self, fgui_text, style_name):
         # 需将字体名添加到font_name_list中，并替换renpy_font_map_definition模板中对应内容。
         if not text_font in self.font_name_list:
             self.font_name_list.append(text_font)
-        self.style_code.append(f"{style_indent}font \"{text_font}\"")
+        self.style_code.append(f"{style_indent}font '{text_font}'")
         self.style_code.append(f"{style_indent}size {fgui_text.font_size}")
-        self.style_code.append(f"{style_indent}color \"{fgui_text.text_color}\"")
+        self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'")
         xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
         self.style_code.append(f"{style_indent}align ({xalign}, {yalign})")
         # Ren'Py中使用两层outlines分别实现投影和描边
@@ -360,9 +360,9 @@ def generate_text_style(self, fgui_text, style_name):
         shadow_width = fgui_text.stroke_size
         has_outline = fgui_text.stroke_color
         if has_shadow:
-            shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+            shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
         if has_outline:
-            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+            stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))"
         if has_shadow and not has_outline:
             self.style_code.append(f"{style_indent}outlines [{shadow_outline}]")
         if not has_shadow and has_outline:
@@ -541,7 +541,7 @@ def generate_button_screen(self, component):
         background = self.default_background
 
         default_title = ''
-        # state_children_dict = self.generate_button_children(component)
+
         for displayable in component.display_list.displayable_list:
             # 不带显示控制器表示始终显示
             if displayable.gear_display is None:
@@ -657,7 +657,9 @@ def generate_image_object(self, image_name, displayable_list):
             elif displayalbe_type == DisplayableChildType.GRAPH:
                 image_definitions.append(f"{self.indent_str}'{displayable.id}'")
             elif displayalbe_type == DisplayableChildType.TEXT:
-                image_definitions.append(f"{self.indent_str}Text({displayable.text})")
+                # image_definitions.append(f"{self.indent_str}Text({displayable.text})")
+                text_displayable_string = self.generate_text_displayable_string(displayable)
+                image_definitions.append(f"{self.indent_str}{text_displayable_string}")
             # 其他类型暂时用空对象占位
             else:
                 image_definitions.append(f"{self.indent_str}Null()")
@@ -668,6 +670,44 @@ def generate_image_object(self, image_name, displayable_list):
         self.renpy_code.extend(image_definitions)
         self.root_indent_level = indent_level
 
+    def generate_text_displayable_string(self, fgui_text):
+        text_displayable_string = ''
+        if not isinstance(fgui_text, FguiText):
+            print("It is not a text displayable.")
+            return text_displayable_string
+        text_anchor_param = f"anchor={fgui_text.pivot}"
+        text_transformanchor = f"transform_anchor=True"
+        text_pos_param = f"pos={fgui_text.xypos}"
+        text_size_param = f"xysize={fgui_text.size}"
+        text_font =  fgui_text.font if fgui_text.font else "SourceHanSansLite"
+        if not text_font in self.font_name_list:
+            self.font_name_list.append(text_font)
+        text_font_param = f"font='{text_font}'"
+        text_font_size_param = f"size={fgui_text.font_size}"
+        text_font_color_param = f"color='{fgui_text.text_color}'"
+        text_textalign_param = f"textalign=0.5"
+        text_bold_param = f"bold={fgui_text.bold}"
+        text_italic_param = f"italic={fgui_text.italic}"
+        text_underline_param = f"underline={fgui_text.underline}"
+        text_strike_param = f"strike={fgui_text.strike}"
+        has_shadow = fgui_text.shadow_color
+        shadow_width = fgui_text.stroke_size
+        has_outline = fgui_text.stroke_color
+        text_outlines_parame = 'outlines=[]'
+        if has_shadow:
+            shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+        if has_outline:
+            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+        if has_shadow and not has_outline:
+            text_outlines_parame = f"outlines=[{shadow_outline}]"
+        if not has_shadow and has_outline:
+            text_outlines_parame = f"outlines=[{stroke_outline}]"
+        if has_shadow and has_outline:
+            text_outlines_parame = f"outlines=[{shadow_outline}, {stroke_outline}]"
+
+        text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})"
+        return text_displayable_string
+
     def generate_image_displayable(self, fgui_image):
         """
         生成图片组件。
@@ -681,7 +721,7 @@ def generate_image_displayable(self, fgui_image):
         for image in self.fgui_assets.package_desc.image_list:
             if fgui_image.src == image.id:
                 image_name = image.name
-                image_code.append(f"{self.indent_str}add \"{image_name}\":")
+                image_code.append(f"{self.indent_str}add '{image_name}':")
                 self.indent_level_up()
                 image_code.append(f"{self.indent_str}pos {fgui_image.xypos}")
                 # 必须指定，旋转和缩放都需要使用。
@@ -731,7 +771,7 @@ def generate_graph_displayable(self, fgui_graph):
             return graph_code
 
         self.renpy_code.extend(self.generate_graph_definitions(fgui_graph))
-        graph_code.append(f"{self.indent_str}add \"{fgui_graph.id}\"")
+        graph_code.append(f"{self.indent_str}add '{fgui_graph.id}'")
 
         return graph_code
 
@@ -770,7 +810,7 @@ def generate_text_displayable(self, fgui_text):
             self.indent_level_down()
 
         else:
-            text_code.append(f"{self.indent_str}text \"{text_str}\":")
+            text_code.append(f"{self.indent_str}text '{text_str}':")
         self.indent_level_up()
         text_code.append(f"{self.indent_str}xysize {fgui_text.size}")
         text_code.append(f"{self.indent_str}pos {fgui_text.xypos}")
@@ -799,22 +839,22 @@ def generate_text_displayable(self, fgui_text):
                 prompt_str = fgui_text.prompt.replace("\n", "\\n").replace("\r", "\\n")
                 text_code.append(f"{self.indent_str}showif check_input_length({fgui_text.name}_input_value):")
                 self.indent_level_up()
-                text_code.append(f"{self.indent_str}text \"{prompt_str}\":")
+                text_code.append(f"{self.indent_str}text '{prompt_str}':")
                 self.indent_level_up()
                 # 暂时使用与input相同样式
                 text_font =  fgui_text.font if fgui_text.font else "SourceHanSansLite"
                 if not text_font in self.font_name_list:
                     self.font_name_list.append(text_font)
-                text_code.append(f"{self.indent_str}font \"{text_font}\"")
+                text_code.append(f"{self.indent_str}font '{text_font}'")
                 text_code.append(f"{self.indent_str}size {fgui_text.font_size}")
-                text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"")
+                text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'")
                 has_shadow = fgui_text.shadow_color
                 shadow_width = fgui_text.stroke_size
                 has_outline = fgui_text.stroke_color
                 if has_shadow:
-                    shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+                    shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
                 if has_outline:
-                    stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+                    stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))"
                 if has_shadow and not has_outline:
                     text_code.append(f"{self.indent_str}outlines [{shadow_outline}]")
                 if not has_shadow and has_outline:
@@ -848,9 +888,9 @@ def generate_text_displayable(self, fgui_text):
         # 需将字体名添加到font_name_list中，并替换renpy_font_map_definition模板中对应内容。
         if not text_font in self.font_name_list:
             self.font_name_list.append(text_font)
-        text_code.append(f"{self.indent_str}font \"{text_font}\"")
+        text_code.append(f"{self.indent_str}font '{text_font}'")
         text_code.append(f"{self.indent_str}size {fgui_text.font_size}")
-        text_code.append(f"{self.indent_str}color \"{fgui_text.text_color}\"")
+        text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'")
         # Ren'Py中使用两层outlines分别实现投影和描边
         # FGUI中的投影包含描边，投影的size等于描边的size，默认值为0；Ren'Py中允许outlines为0，依然有效果。
         # Ren'Py中的property outlines是列表，先投影后描边。
@@ -858,9 +898,9 @@ def generate_text_displayable(self, fgui_text):
         shadow_width = fgui_text.stroke_size
         has_outline = fgui_text.stroke_color
         if has_shadow:
-            shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+            shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
         if has_outline:
-            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+            stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))"
         if has_shadow and not has_outline:
             text_code.append(f"{self.indent_str}outlines [{shadow_outline}]")
         if not has_shadow and has_outline:

From 8076bc9db65ea5dd30eea791b928b6edb481a8f6 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Wed, 5 Nov 2025 15:17:25 +0800
Subject: [PATCH 08/40] Update Text drop-shadow outline method

Add a extra outline to keep consistent with FGUI Text Component view.
---
 .../utils/renpy/Fgui2RenpyConverter.py        | 97 +++++++------------
 1 file changed, 36 insertions(+), 61 deletions(-)

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index dfa6605..d3391a2 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -353,22 +353,10 @@ def generate_text_style(self, fgui_text, style_name):
         self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'")
         xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
         self.style_code.append(f"{style_indent}align ({xalign}, {yalign})")
-        # Ren'Py中使用两层outlines分别实现投影和描边
-        # FGUI中的投影包含描边，投影的size等于描边的size，默认值为0；Ren'Py中允许outlines为0，依然有效果
-        # Ren'Py中的property outlines可以是列表，先投影后描边
-        has_shadow = fgui_text.shadow_color
-        shadow_width = fgui_text.stroke_size
-        has_outline = fgui_text.stroke_color
-        if has_shadow:
-            shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
-        if has_outline:
-            stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))"
-        if has_shadow and not has_outline:
-            self.style_code.append(f"{style_indent}outlines [{shadow_outline}]")
-        if not has_shadow and has_outline:
-            self.style_code.append(f"{style_indent}outlines [{stroke_outline}]")
-        if has_shadow and has_outline:
-            self.style_code.append(f"{style_indent}outlines [{shadow_outline}, {stroke_outline}]")
+
+        text_outline_string = self.generate_text_outline_string(fgui_text)
+        self.style_code.append(f"{style_indent}outlines {text_outline_string}")
+
         # 默认两侧居中对齐
         self.style_code.append(f"{style_indent}textalign 0.5")
         # 粗体、斜体、下划线、删除线
@@ -642,8 +630,8 @@ def generate_image_object(self, image_name, displayable_list):
         if len(displayable_list) <= 0:
             return
         image_definitions = []
-        image_definitions.append("# 图像定义")
-        image_definitions.append("# 合成的图像对象")
+        image_definitions.append("# image对象定义")
+        image_definitions.append("# 使用其他image对象的组合")
         indent_level = self.root_indent_level
         self.reset_indent_level()
         image_definitions.append(f"image {image_name}:")
@@ -670,6 +658,26 @@ def generate_image_object(self, image_name, displayable_list):
         self.renpy_code.extend(image_definitions)
         self.root_indent_level = indent_level
 
+    @staticmethod
+    def generate_text_outline_string(fgui_text):
+        outline_string = '[]'
+        has_shadow = fgui_text.shadow_color
+        has_outline = fgui_text.stroke_color
+        if has_shadow:
+            shadow_outline = f"(absolute(0), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+        if has_outline:
+            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
+        if has_shadow and not has_outline:
+            outline_string = f"[{shadow_outline}]"
+        if not has_shadow and has_outline:
+            outline_string = f"[{stroke_outline}]"
+        # Ren'Py中的投影不包含其他描边。为保持与FGUI效果一直，需要添加一层，宽度与描边一致，颜色、偏移与投影一致。
+        if has_shadow and has_outline:
+            extra_shadow = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
+            outline_string = f"[{shadow_outline}, {extra_shadow}, {stroke_outline}]"
+        return outline_string
+
+
     def generate_text_displayable_string(self, fgui_text):
         text_displayable_string = ''
         if not isinstance(fgui_text, FguiText):
@@ -690,20 +698,9 @@ def generate_text_displayable_string(self, fgui_text):
         text_italic_param = f"italic={fgui_text.italic}"
         text_underline_param = f"underline={fgui_text.underline}"
         text_strike_param = f"strike={fgui_text.strike}"
-        has_shadow = fgui_text.shadow_color
-        shadow_width = fgui_text.stroke_size
-        has_outline = fgui_text.stroke_color
-        text_outlines_parame = 'outlines=[]'
-        if has_shadow:
-            shadow_outline = f"(absolute({shadow_width}), \"{fgui_text.shadow_color}\", absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
-        if has_outline:
-            stroke_outline = f"(absolute({fgui_text.stroke_size}), \"{fgui_text.stroke_color}\", absolute(0), absolute(0))"
-        if has_shadow and not has_outline:
-            text_outlines_parame = f"outlines=[{shadow_outline}]"
-        if not has_shadow and has_outline:
-            text_outlines_parame = f"outlines=[{stroke_outline}]"
-        if has_shadow and has_outline:
-            text_outlines_parame = f"outlines=[{shadow_outline}, {stroke_outline}]"
+
+        text_outline_string = self.generate_text_outline_string(fgui_text)
+        text_outlines_parame = 'outlines={text_outline_string}'
 
         text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})"
         return text_displayable_string
@@ -848,19 +845,9 @@ def generate_text_displayable(self, fgui_text):
                 text_code.append(f"{self.indent_str}font '{text_font}'")
                 text_code.append(f"{self.indent_str}size {fgui_text.font_size}")
                 text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'")
-                has_shadow = fgui_text.shadow_color
-                shadow_width = fgui_text.stroke_size
-                has_outline = fgui_text.stroke_color
-                if has_shadow:
-                    shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
-                if has_outline:
-                    stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))"
-                if has_shadow and not has_outline:
-                    text_code.append(f"{self.indent_str}outlines [{shadow_outline}]")
-                if not has_shadow and has_outline:
-                    text_code.append(f"{self.indent_str}outlines [{stroke_outline}]")
-                if has_shadow and has_outline:
-                    text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]")
+                text_outline_string = self.generate_text_outline_string(fgui_text)
+                text_code.append(f"{self.indent_str}outlines {text_outline_string}")
+
                 if fgui_text.letter_spacing:
                     text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}")
                 if fgui_text.leading:
@@ -891,22 +878,10 @@ def generate_text_displayable(self, fgui_text):
         text_code.append(f"{self.indent_str}font '{text_font}'")
         text_code.append(f"{self.indent_str}size {fgui_text.font_size}")
         text_code.append(f"{self.indent_str}color '{fgui_text.text_color}'")
-        # Ren'Py中使用两层outlines分别实现投影和描边
-        # FGUI中的投影包含描边，投影的size等于描边的size，默认值为0；Ren'Py中允许outlines为0，依然有效果。
-        # Ren'Py中的property outlines是列表，先投影后描边。
-        has_shadow = fgui_text.shadow_color
-        shadow_width = fgui_text.stroke_size
-        has_outline = fgui_text.stroke_color
-        if has_shadow:
-            shadow_outline = f"(absolute({shadow_width}), '{fgui_text.shadow_color}', absolute({fgui_text.shadow_offset[0]}), absolute({fgui_text.shadow_offset[1]}))"
-        if has_outline:
-            stroke_outline = f"(absolute({fgui_text.stroke_size}), '{fgui_text.stroke_color}', absolute(0), absolute(0))"
-        if has_shadow and not has_outline:
-            text_code.append(f"{self.indent_str}outlines [{shadow_outline}]")
-        if not has_shadow and has_outline:
-            text_code.append(f"{self.indent_str}outlines [{stroke_outline}]")
-        if has_shadow and has_outline:
-            text_code.append(f"{self.indent_str}outlines [{shadow_outline}, {stroke_outline}]")
+
+        text_outline_string = self.generate_text_outline_string(fgui_text)
+        text_code.append(f"{self.indent_str}outlines {text_outline_string}")
+
         # 字间距与行距
         if fgui_text.letter_spacing:
             text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}")

From f61503017a9bd5bacbc9c8421e787d76b52fbac8 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 7 Nov 2025 23:56:14 +0800
Subject: [PATCH 09/40] Support  "Slider" component

1. Add "Slider" converting.
2. Graph name add component name as prefix, because of same graph id within different components.
3. Support color hex string of 6 characters(rrggbb), with default alpha value(ff).
4. Add simple slider property and "FguiRelation" class.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  68 +++-
 .../utils/renpy/Fgui2RenpyConverter.py        | 317 ++++++++++++++----
 2 files changed, 316 insertions(+), 69 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 34e18cb..7be9ef2 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+from re import M
 import sys
 import os
 from lxml import etree
@@ -252,7 +253,8 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
         self.id = id
         self.name = name
         self.package_desc_id = package_desc_id
-        self.size = component_etree.get("size")
+        size = component_etree.get("size")
+        self.size = tuple(map(int, size.split(","))) if size else (0,0)
         self.extention = component_etree.get("extention")
         self.mask = component_etree.get("mask")
         # 轴心。默认值为(0.0, 0.0)。
@@ -329,7 +331,7 @@ class FguiProgressBar(FguiComponent):
     """
     def __init__(self, component_etree, id, name, package_desc_id=None):
         super().__init__(component_etree, id, name, package_desc_id=None)
-        combobox = component_etree.find("ProgressBar")
+        self.progressbar = component_etree.find("ProgressBar")
 
 class FguiSlider(FguiComponent):
     """
@@ -339,7 +341,8 @@ class FguiSlider(FguiComponent):
     """
     def __init__(self, component_etree, id, name, package_desc_id=None):
         super().__init__(component_etree, id, name, package_desc_id=None)
-        combobox = component_etree.find("Slider")
+        self.slider = component_etree.find("Slider")
+        self.title_type = self.slider.get("titleType")
 
 class FguiWindow(FguiComponent):
     """
@@ -370,7 +373,7 @@ def __init__(self, display_list_etree, package_desc_id=None):
         # print(f"package_desc_id: {package_desc_id}")
         self.displayable_list = []
         for displayable in self.display_list_etree:
-            print(displayable.tag)
+            # print(displayable.tag)
             # 根据标签类型创建相应的FguiDisplayable对象
             if displayable.tag == "graph":
                 self.displayable_list.append(FguiGraph(displayable))
@@ -388,14 +391,19 @@ def __init__(self, display_list_etree, package_desc_id=None):
 
 def hex_aarrggbb_to_rgba(hex_color):
     """
-    将一个 8 位的十六进制颜色字符串 (AARRGGBB) 转换为一个 RGBA 元组。
+    将一个8位的十六进制颜色字符串(AARRGGBB)或6位的十六进制颜色字符串(RRGGBB)转换为一个 RGBA 元组。
     """
     # 移除字符串头部的 '#'
-    clean_hex = hex_color.lstrip('#')
+    clean_hex = hex_color.lstrip('#').lower()
+    hex_str_len = len(clean_hex)
 
-    # 检查处理后的字符串长度是否为 8
-    if len(clean_hex) != 8:
-        raise ValueError("输入的十六进制字符串必须是8位 (AARRGGBB)")
+    # 检查处理后的字符串长度是否为8或6
+    if hex_str_len != 8 and hex_str_len != 6:
+        raise ValueError("输入的十六进制字符串必须是8位(AARRGGBB)或6位(RRGGBB)")
+
+    # 6位字符则加上alpha通道的默认值 ff
+    if hex_str_len ==6:
+        clean_hex = 'ff' + clean_hex
 
     # 按 AARRGGBB 的顺序提取，并从16进制转换为10进制整数
     try:
@@ -460,7 +468,7 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.xypos = tuple(map(int, xy.split(",")))
         # 尺寸。若为None：image的size默认与image对象一致。
         size = self.display_item_tree.get("size")
-        self.size = tuple(map(int, size.split(","))) if size else None
+        self.size = tuple(map(int, size.split(","))) if size else (0,0)
         # 保持比例。若为None，则将宽和高分别缩放到size。
         self.aspect = (self.display_item_tree.get("aspect") == "true")
         # 缩放。默认值为(1.0, 1.0)。
@@ -502,6 +510,9 @@ def __init__(self, display_item_tree, package_description_id=None):
         # Button，按钮专有属性
         self.button_property = None
 
+        # Slider，滑动条专有属性
+        self.slider_property = None
+
         # gear属性
         self.gear_display = None
         self.gear_display_2 = None
@@ -512,9 +523,12 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.gear_text = None
         self.gear_icon = None
 
+        # relation
+        self.relations = None
+
         # 一级子对象
         self.child_num = len(self.display_item_tree)
-        # 控制器gear子组件
+        # 控制器gear子组件和relation关联项
         for i in range(self.child_num):
             if self.display_item_tree[i].tag == "gearDisplay" :
                 self.gear_display = FguiGearDisplay(self.display_item_tree[i])
@@ -534,6 +548,10 @@ def __init__(self, display_item_tree, package_description_id=None):
                 self.gear_icon = FguiGearIcon(self.display_item_tree[i])
             elif self.display_item_tree[i].tag == "Button" :
                 self.button_property = FguiButtonProperty(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "relation" :
+                self.relations = FguiRelation(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "Slider" :
+                self.slider_property = FguiSliderProperty(self.display_item_tree[i])
             else:
                 print(f"Tag not parse: {self.display_item_tree[i].tag}.")
 
@@ -755,6 +773,34 @@ def __init__(self, display_item_tree, package_description_id=None):
     def get_item_id(self, packageDescription_id):
         self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):]
 
+class FguiRelation:
+    """
+    组件关联属性对象。表示与其他组件的相对关系。
+    通常是一个target：“关联对象”-sidePair“关联方式”的类字典结构。
+    """
+    def __init__(self, relation_item_tree):
+        if relation_item_tree.tag != "relation":
+            raise ValueError("xml tag is not relation.")
+        self.relation_item_tree = relation_item_tree
+        self.relation_dict = {}
+        key = self.relation_item_tree.get("target")
+        value = self.relation_item_tree.get("sidePair")
+        self.relation_dict[key] = value
+        # print(self.relation_dict)
+
+class FguiSliderProperty:
+    """
+    滑块的数值属性。分别包含最小值、最大值与当前值。
+    如果某一项属性未出现则等于默认值0。
+    """
+    def __init__(self, slider_property_tree):
+        if slider_property_tree.tag != "Slider":
+            raise ValueError("xml tag is not Slider.")
+        self.slider_property_tree = slider_property_tree
+        self.current_value = self.slider_property_tree.get("value", 0)
+        self.min_value = self.slider_property_tree.get("min", 0)
+        self.max_value = self.slider_property_tree.get("max", 0)
+
 class FguiGearBase:
     """
     Displayable控制器设置相关的基类。
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index d3391a2..f64a425 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
+import dis
 import sys
 import os
 import re
@@ -8,7 +9,9 @@
 
 import shutil
 from enum import IntEnum
+from tkinter import HORIZONTAL, NO, VERTICAL
 
+# 复合型组件的子组件枚举类型
 class DisplayableChildType(IntEnum):
     NULL = 0
     IMAGE = 1
@@ -17,6 +20,11 @@ class DisplayableChildType(IntEnum):
     COMPONENT = 4
     OTHER = 5
 
+# bar、scrollbar、slider等组件的方向枚举类型
+class BarOrientationType(IntEnum):
+    HORIZONTAL = 0
+    VERTICAL = 1
+
 # 添加当前目录到Python路径，以便导入FguiAssetsParseLib
 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 
@@ -28,6 +36,23 @@ class FguiToRenpyConverter:
     将FairyGUI资源转换为Ren'Py的screen语言
     """
 
+    # 静态变量
+    # 文本对齐的转义字典
+    align_dict = {"left": 0.0, "center": 0.5, "right": 1.0, "top": 0.0, "middle": 0.5, "bottom": 1.0}
+    # 4个空格作为缩进基本单位
+    indent_unit = '    '
+
+    # 默认背景色，在某些未指定image的情况下用作填充。
+    default_background = '#fff'
+
+    # 菜单类界面名称，需要添加 tag menu。
+    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences']
+
+    # 模态类界面名称，需要添加 Modal True。
+    modal_screen_name_list = ['confirm']
+
+
+    
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
         self.renpy_code = []
@@ -42,22 +67,14 @@ def __init__(self, fgui_assets):
         self.screen_has_dismiss = False
         self.dismiss_action_list = []
 
-        # 文本对齐的转义字典
-        self.align_dict = {"left": 0.0, "center": 0.5, "right": 1.0, "top": 0.0, "middle": 0.5, "bottom": 1.0}
-
         # 字体名称列表。SourceHanSansLite为Ren'Py默认字体。
         self.font_name_list = ["SourceHanSansLite"]
 
-        # 4个空格作为缩进基本单位
-        self.indent_unit = '    '
         # 组件缩进级别
         self.root_indent_level = 0
         # 缩进字符串
         self.indent_str = ''
 
-        # 默认背景色，在某些未指定image的情况下用作填充。
-        self.default_background = '#fff'
-
         # 部分模板与预设目录
         # self.renpy_template_dir = 'renpy_templates'
         self.renpy_template_dir = os.environ.get('RENPY_TEMPLATES_DIR', 
@@ -140,9 +157,10 @@ def generate_image_definitions(self):
         image_definitions.append("")
         self.renpy_code.extend(image_definitions)
 
-    def generate_graph_definitions(self, fgui_graph):
+    def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : str) -> list:
         """
-        生成图形组件定义。图形组件有多种类别：
+        生成图形组件定义。返回字符串用于非screen定义。
+        图形组件有多种类别：
         None: 空白
         rect: 矩形(可带圆角)
         eclipse: 椭圆(包括圆形)
@@ -166,13 +184,13 @@ def generate_graph_definitions(self, fgui_graph):
         # 空白使用Null。
         if fgui_graph.type is None:
             # graph_code.append(f"{self.indent_str}null width {fgui_graph.size[0]} height {fgui_graph.size[1]}")
-            self.graph_template_dict['null'].replace('{image_name}', fgui_graph.id)\
+            self.graph_template_dict['null'].replace('{image_name}', f"{component_name}_{fgui_graph.id}")\
                                 .replace('{width}', str(fgui_graph.size[0]))\
                                 .replace('{height}', str(fgui_graph.size[1]))
         # 矩形(圆边矩形)。原生组件不支持圆边矩形，使用自定义shader实现。
         elif fgui_graph.type == "rect":
             # renpy_rectangle_template.txt模板已在转换器初始化读取。模板代码 self.self.graph_template_dict['rectangle'] 。
-            graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', fgui_graph.id)\
+            graph_img_def = self.graph_template_dict['rectangle'].replace('{image_name}', f"{component_name}_{fgui_graph.id}")\
                                 .replace('{rectangle_color}', str(rgba_normalize(fgui_graph.fill_color)))\
                                 .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
                                 .replace('{image_size}', str(fgui_graph.size))\
@@ -190,7 +208,7 @@ def generate_graph_definitions(self, fgui_graph):
                                 .replace('{yzoom}', str(fgui_graph.scale[1]))
         # 椭圆(圆形)。Ren'Py尚不支持椭圆，使用自定义shader实现。
         elif fgui_graph.type == "eclipse":
-            graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', fgui_graph.id)\
+            graph_img_def = self.graph_template_dict['ellipse'].replace('{image_name}', f"{component_name}_{fgui_graph.id}")\
                                 .replace('{ellipse_color}', str(rgba_normalize(fgui_graph.fill_color)))\
                                 .replace('{stroke_color}', str(rgba_normalize(fgui_graph.stroke_color)))\
                                 .replace('{image_size}', str(fgui_graph.size))\
@@ -213,6 +231,148 @@ def generate_graph_definitions(self, fgui_graph):
 
         return graph_code
 
+    def generate_slider_style(self, fgui_slider : FguiSlider):
+        """
+        生成滑块样式。
+        Ren'Py生成的滑动条效果与FGUI略有不同。
+        FGUI的左侧激活状态bar会在水平方向随滑块缩放，Ren'Py不会缩放。
+        目标样例：
+        image horizontal_idle_thumb_image:
+            "horizontal_thumb_base"
+
+        image horizontal_hover_thumb_image:
+            "horizontal_thumb_active"
+
+        image horizontal_idle_bar_image:
+            "horizontal_bar_base"
+
+        image horizontal_hover_bar_image:
+            "horizontal_bar_active"
+
+        style horizontal_slider:
+            bar_vertical False
+            xsize 540
+            ysize 3
+            thumb_offset 24
+            left_bar "horizontal_hover_bar_image"
+            right_bar "horizontal_idle_bar_image"
+            thumb Fixed(Frame("horizontal_[prefix_]thumb_image",xsize=48,ysize=54,ypos=-22))
+        """
+        bar_image_definition_code = []
+        style_definition_code = []
+        slider_style_code = []
+        if not isinstance(fgui_slider, FguiSlider):
+            print("It is not a slider.")
+            return slider_style_code
+
+        # 默认为水平滑块。实际需要根据grip中relation的sidePair属性来确定。
+        slider_type = BarOrientationType.HORIZONTAL
+        # bar的第一段，水平方向为right_bar，垂直方向为top_bar
+        first_bar_name = ''
+        # bar的第二段，水平方向为left_bar，垂直方向为bottom_bar
+        second_bar_name = ''
+        # 滑块图片名
+        thumb_idle_name = ''
+        thumb_hover_name = ''
+
+        # bar id，在grip的relation_dict中作为key查找值
+        bar_id = ''
+        grip_com = None
+        # 滑块位置，用做偏移
+        thumb_xpos, thumb_ypos = 0, 0
+
+        # slider 组件固定由两张图片(图形)和一个按钮构成，可能还有一个文本组件。
+        # 图片(图形)固定名称为n0和bar，按钮固定名称grip，文本组件固定名称title。
+        # 其他组件暂不处理。
+        for displayable in fgui_slider.display_list.displayable_list:
+            # FGUI中滑动条的背景
+            if displayable.name == 'n0':
+                if isinstance(displayable, FguiImage):
+                    second_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                elif isinstance(displayable, FguiGraph):
+                    bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_slider.name))
+                    second_bar_name = f"{fgui_slider.name}_{displayable.id}"
+                else:
+                    print("Slider base is neither image nor graph.")
+                    return slider_style_code
+            # FGUI中滑动条的可变bar部分
+            if displayable.name == 'bar':
+                if isinstance(displayable, FguiImage):
+                    first_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                elif isinstance(displayable, FguiGraph):
+                    bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_slider.name))
+                    first_bar_name = f"{fgui_slider.name}_{displayable.id}"
+                else:
+                    print("Slider bar is neither image nor graph.")
+                    return slider_style_code
+                bar_id = displayable.id
+            # FGUI中滑动条的标题类型文本
+            if displayable.name == 'title':
+                if isinstance(displayable, FguiText):
+                    style_name = f"slider_{fgui_slider.id}_title"
+                    self.generate_text_style(displayable, style_name)
+                else:
+                    print("Slider title is not text.")
+                    return slider_style_code
+            # FGUI中滑动条的滑块按钮
+            if displayable.name == 'grip':
+                grip_com = self.fgui_assets.get_component_by_id(displayable.src)
+                if isinstance(grip_com, FguiButton):
+                    # grip是按钮，会生成对应的image对象
+                    thumb_idle_name = f"{fgui_slider.name}_grip_idle_background"
+                    thumb_hover_name = f"{fgui_slider.name}_grip_hover_background"
+                    thumb_xpos, thumb_ypos = displayable.xypos
+                    # 根据grip中relation的sidePair属性来确定方向
+                    side_pair_str = displayable.relations.relation_dict[bar_id]
+                    # 只有该值表示垂直滑动条
+                    if side_pair_str == "bottom-bottom":
+                        slider_type = BarOrientationType.VERTICAL
+                else:
+                    print("Slider grp is not button.")
+                    return slider_style_code
+
+        # 生成bar和thumb的image
+        bar_image_definition_code.append(f"image {fgui_slider.name}_base_bar_image:")
+        bar_image_definition_code.append(f"{self.indent_unit}'{second_bar_name}'")
+        bar_image_definition_code.append(f"image {fgui_slider.name}_active_bar_image:")
+        bar_image_definition_code.append(f"{self.indent_unit}'{first_bar_name}'")
+        bar_image_definition_code.append(f"image {fgui_slider.name}_idle_thumb_image:")
+        bar_image_definition_code.append(f"{self.indent_unit}'{thumb_idle_name}'")
+        bar_image_definition_code.append(f"image {fgui_slider.name}_hover_thumb_image:")
+        bar_image_definition_code.append(f"{self.indent_unit}'{thumb_hover_name}'")
+        bar_image_definition_code.append("")
+
+        is_vertical = (slider_type==BarOrientationType.VERTICAL)
+        thumb_offset = int(grip_com.size[1]/2) if is_vertical else int(grip_com.size[0]/2)
+        style_definition_code.append(f"style {fgui_slider.name}:")
+        style_definition_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
+        style_definition_code.append(f"{self.indent_unit}xysize {fgui_slider.size}")
+        style_definition_code.append(f"{self.indent_unit}thumb_offset {thumb_offset}")
+        if is_vertical:
+            style_definition_code.append(f"{self.indent_unit}top_bar '{second_bar_name}'")
+            style_definition_code.append(f"{self.indent_unit}bottom_bar '{first_bar_name}'")
+            thumb_ypos = 0
+        else:
+            style_definition_code.append(f"{self.indent_unit}left_bar '{first_bar_name}'")
+            style_definition_code.append(f"{self.indent_unit}right_bar '{second_bar_name}'")
+            thumb_xpos = 0
+        style_definition_code.append(f"{self.indent_unit}thumb Fixed(Frame('{fgui_slider.name}_[prefix_]thumb_image',xysize={grip_com.size},pos=({thumb_xpos},{thumb_ypos})))")
+        style_definition_code.append("")
+
+        slider_style_code.extend(bar_image_definition_code)
+        slider_style_code.extend(style_definition_code)
+
+        self.style_code.extend(slider_style_code)
+
+
+    @staticmethod
+    def is_menu_screen(screen_name):
+        return screen_name in FguiToRenpyConverter.menu_screen_name_list
+    
+    @staticmethod
+    def is_modal_screen(screen_name):
+        return screen_name in FguiToRenpyConverter.modal_screen_name_list
+
     def generate_screen(self, component):
         """
         生成screen定义。目标样例：
@@ -254,16 +414,27 @@ def generate_screen(self, component):
         id = component.id
         screen_name = component.name
 
+        # 界面入参列表
+        screen_params = ''
+        # confirm 界面固定入参
+        if screen_name == 'confirm':
+            screen_params = 'message, yes_action, no_action'
+
         self.reset_indent_level()
-        self.screen_definition_head.append(f"screen {screen_name}():")
+        self.screen_definition_head.append(f"screen {screen_name}({screen_params}):")
         self.indent_level_up()
+        if self.is_menu_screen(screen_name):
+            self.screen_ui_code.append(f"{self.indent_str}tag menu\n")
+        if self.is_modal_screen(screen_name):
+            self.screen_ui_code.append(f"{self.indent_str}modal True")
+            self.screen_ui_code.append(f"{self.indent_str}zorder 200\n")
         for displayable in component.display_list.displayable_list:
             # 图片组件
             if isinstance(displayable, FguiImage):
                 self.screen_ui_code.extend(self.generate_image_displayable(displayable))
             # 图形组件
             elif isinstance(displayable, FguiGraph):
-                self.screen_ui_code.extend(self.generate_graph_displayable(displayable))
+                self.screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name))
             # 文本组件
             elif isinstance(displayable, FguiText):
                 self.screen_ui_code.extend(self.generate_text_displayable(displayable))
@@ -283,15 +454,20 @@ def generate_screen(self, component):
                     self.indent_level_up()
                     self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
-                    parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data)
-                    self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{displayable.id}'")
+                    if displayable.button_property:
+                        parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data)
+                    else:
+                        parameter_str = self.generate_button_parameter(None, displayable.custom_data)
+                    self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'")
                     self.indent_level_down()
 
         self.screen_code.extend(self.screen_definition_head)
-        self.screen_code.extend(self.screen_variable_code)
-        self.screen_code.append("")
-        self.screen_code.extend(self.screen_function_code)
-        self.screen_code.append("")
+        if self.screen_variable_code:
+            self.screen_code.extend(self.screen_variable_code)
+            self.screen_code.append("")
+        if self.screen_function_code:
+            self.screen_code.extend(self.screen_function_code)
+            self.screen_code.append("")
         # 添加只有1个生效的dismiss
         if self.screen_has_dismiss:
             self.screen_ui_code.append(f"{self.indent_str}dismiss:")
@@ -299,9 +475,12 @@ def generate_screen(self, component):
             self.screen_ui_code.append(f"{self.indent_str}modal False")
             dismiss_action_list = ', '.join(self.dismiss_action_list)
             self.screen_ui_code.append(f"{self.indent_str}action [{dismiss_action_list}]")
-            
+
+        self.screen_ui_code.append("")
         self.screen_code.extend(self.screen_ui_code)
 
+
+
     def generate_button_parameter(self, button_title=None, original_actions_str=None):
         parameter_str = ""
         title_str = ""
@@ -317,7 +496,7 @@ def generate_button_parameter(self, button_title=None, original_actions_str=None
 
         return parameter_str
 
-    def generate_text_style(self, fgui_text, style_name):
+    def generate_text_style(self, fgui_text : FguiText, style_name : str):
         """
         生成文本样式，专用于按钮标题，因为按钮中通常只有一个文本组件。
         目标样例：
@@ -331,14 +510,14 @@ def generate_text_style(self, fgui_text, style_name):
             textalign 0.5
         """
 
-        self.style_code.clear()
+        # self.style_code.clear()
         # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
         if not isinstance(fgui_text, FguiText):
             print("It is not a text displayable.")
             return
         # 样式具有固定一档的缩进
         style_indent = "    "
-        default_title = fgui_text.text
+        # default_title = fgui_text.text
         # 定义样式
         self.style_code.append(f"style {style_name}:")
         self.style_code.append(f"{style_indent}xysize {fgui_text.size}")
@@ -357,8 +536,12 @@ def generate_text_style(self, fgui_text, style_name):
         text_outline_string = self.generate_text_outline_string(fgui_text)
         self.style_code.append(f"{style_indent}outlines {text_outline_string}")
 
+
+        #设置最小宽度才能使text_align生效
+        self.style_code.append(f"{style_indent}min_width {fgui_text.size[0]}")
+        xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
         # 默认两侧居中对齐
-        self.style_code.append(f"{style_indent}textalign 0.5")
+        self.style_code.append(f"{style_indent}textalign {xalign}")
         # 粗体、斜体、下划线、删除线
         if fgui_text.bold:
             self.style_code.append(f"{style_indent}bold {fgui_text.bold}")
@@ -383,7 +566,7 @@ def get_child_type(displayable):
         else:
             return DisplayableChildType.OTHER
 
-    def generate_button_children(self, fgui_button):
+    def generate_button_children(self, fgui_button : FguiButton):
         """
         生成按钮各状态的子组件。
         """
@@ -428,18 +611,22 @@ def generate_button_children(self, fgui_button):
 
         # 将displayable_list中的子组件按状态分别添加到对应列表中
         for displayable in fgui_button.display_list.displayable_list:
+            displayable_id = ''
+            if isinstance(displayable, FguiGraph):
+                displayable_id = f"{fgui_button.name}_{displayable.id}"
+            else:
+                displayable_id = displayable.id
             if displayable.gear_display is None:
-                state_children_dict['always_show'].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable)))
+                state_children_dict['always_show'].append((displayable_id, FguiToRenpyConverter.get_child_type(displayable)))
                 break
             for i in range(0, state_number):
                 if displayable.gear_display and str(i) in displayable.gear_display.controller_index:
-                    state_children_dict[state_index_name_dict[i]].append((displayable.id, FguiToRenpyConverter.get_child_type(displayable)))
+                    state_children_dict[state_index_name_dict[i]].append((displayable_id, FguiToRenpyConverter.get_child_type(displayable)))
                     continue
         # print(state_children_dict)
 
         return state_children_dict
 
-
     def generate_button_screen(self, component):
 
         """
@@ -470,7 +657,7 @@ def generate_button_screen(self, component):
         self.screen_ui_code.clear()
         self.has_dismiss = False
         self.dismiss_action_list.clear()
-        self.style_code.clear()
+        # self.style_code.clear()
 
         # 4种状态的子组件列表
         idle_child_list = []
@@ -554,7 +741,7 @@ def generate_button_screen(self, component):
                         break
                 # print(image_id_name_mapping)
             elif isinstance(displayable, FguiGraph):
-                self.renpy_code.extend(self.generate_graph_definitions(displayable))
+                self.renpy_code.extend(self.generate_graph_definitions(displayable, button_name))
                 # break
             # 文本组件。只处理名为title的文本组件。其他的文本待后续增加。
             # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
@@ -568,15 +755,15 @@ def generate_button_screen(self, component):
 
         # 根据state_children_dict生成各种对应状态的background
         # idle_background
-        self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'])
+        self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name)
         # selected_background
-        self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'])
+        self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name)
         # hover_background
-        self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'],)
+        self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name)
         # selected_hover_background
-        self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'])
+        self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name)
         # insensitive_background
-        self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'])
+        self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name)
 
 
         # 重置缩进级别
@@ -592,17 +779,17 @@ def generate_button_screen(self, component):
         self.indent_level_up()
         self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'")
         self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})")
-        # self.screen_ui_code.append(f"{self.indent_str}background '{background}'")
-        if state_children_dict['idle']:
-            self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'")
-        if state_children_dict['selected']:
-            self.screen_ui_code.append(f"{self.indent_str}selected_background '{button_name}_selected_background'")
-        if state_children_dict['hover']:
-            self.screen_ui_code.append(f"{self.indent_str}hover_background '{button_name}_hover_background'")
-        if state_children_dict['selected_hover']:
-            self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'")
-        if state_children_dict['insensitive']:
-            self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'")
+        self.screen_ui_code.append(f"{self.indent_str}background '{button_name}_[prefix_]background'")
+        # if state_children_dict['idle']:
+        #     self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'")
+        # if state_children_dict['selected']:
+        #     self.screen_ui_code.append(f"{self.indent_str}selected_background '{button_name}_selected_background'")
+        # if state_children_dict['hover']:
+        #     self.screen_ui_code.append(f"{self.indent_str}hover_background '{button_name}_hover_background'")
+        # if state_children_dict['selected_hover']:
+        #     self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'")
+        # if state_children_dict['insensitive']:
+        #     self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'")
         self.screen_ui_code.append(f"{self.indent_str}text title")
         self.screen_ui_code.append(f"{self.indent_str}action actions")
         # 一些按钮特性
@@ -622,7 +809,7 @@ def generate_button_screen(self, component):
     def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"):
         return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5)
 
-    def generate_image_object(self, image_name, displayable_list):
+    def generate_image_object(self, image_name : str, displayable_list : list, component_name: str):
         """
         生成image对象，入参为一些子组件列表，自带transform。
         暂时不考虑允许列表中的子组件添加额外transform，只单纯堆叠。
@@ -643,7 +830,7 @@ def generate_image_object(self, image_name, displayable_list):
                 name = self.fgui_assets.get_componentname_by_id(displayable.src)
                 image_definitions.append(f"{self.indent_str}'{name}'")
             elif displayalbe_type == DisplayableChildType.GRAPH:
-                image_definitions.append(f"{self.indent_str}'{displayable.id}'")
+                image_definitions.append(f"{self.indent_str}'{component_name}_{displayable.id}'")
             elif displayalbe_type == DisplayableChildType.TEXT:
                 # image_definitions.append(f"{self.indent_str}Text({displayable.text})")
                 text_displayable_string = self.generate_text_displayable_string(displayable)
@@ -661,6 +848,9 @@ def generate_image_object(self, image_name, displayable_list):
     @staticmethod
     def generate_text_outline_string(fgui_text):
         outline_string = '[]'
+        if not isinstance(fgui_text, FguiText):
+            print("It is not a text Displayable.")
+            return outline_string
         has_shadow = fgui_text.shadow_color
         has_outline = fgui_text.stroke_color
         if has_shadow:
@@ -693,7 +883,9 @@ def generate_text_displayable_string(self, fgui_text):
         text_font_param = f"font='{text_font}'"
         text_font_size_param = f"size={fgui_text.font_size}"
         text_font_color_param = f"color='{fgui_text.text_color}'"
-        text_textalign_param = f"textalign=0.5"
+        text_min_width_param = f"min_width={fgui_text.size[0]}"
+        xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
+        text_textalign_param = f"textalign={xalign}"
         text_bold_param = f"bold={fgui_text.bold}"
         text_italic_param = f"italic={fgui_text.italic}"
         text_underline_param = f"underline={fgui_text.underline}"
@@ -702,7 +894,7 @@ def generate_text_displayable_string(self, fgui_text):
         text_outline_string = self.generate_text_outline_string(fgui_text)
         text_outlines_parame = 'outlines={text_outline_string}'
 
-        text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})"
+        text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_min_width_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})"
         return text_displayable_string
 
     def generate_image_displayable(self, fgui_image):
@@ -752,9 +944,10 @@ def generate_image_displayable(self, fgui_image):
                 break
         return image_code
 
-    def generate_graph_displayable(self, fgui_graph):
+    def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : str) -> list:
         """
-        生成图形组件。图形组件有多种类别：
+        生成图形组件。用于screen中。
+        图形组件有多种类别：
         None: 空白
         rect: 矩形(可带圆角)
         eclipse: 椭圆(包括圆形)
@@ -767,8 +960,8 @@ def generate_graph_displayable(self, fgui_graph):
             print("It is not a graph displayable.")
             return graph_code
 
-        self.renpy_code.extend(self.generate_graph_definitions(fgui_graph))
-        graph_code.append(f"{self.indent_str}add '{fgui_graph.id}'")
+        self.renpy_code.extend(self.generate_graph_definitions(fgui_graph, component_name))
+        graph_code.append(f"{self.indent_str}add '{component_name}_{fgui_graph.id}'")
 
         return graph_code
 
@@ -805,7 +998,10 @@ def generate_text_displayable(self, fgui_text):
             self.indent_level_up()
             text_code.append(f"{self.indent_str}action {fgui_text.name}_input_value.Enable()")
             self.indent_level_down()
-
+        # message 表示可能需要接收Ren'Py的提示语
+        elif fgui_text.name == 'message':
+            text_code.append(f"{self.indent_str}text message:")
+            print("Detected a 'message' text displayable. Please ensure it was used in confirm screen.")
         else:
             text_code.append(f"{self.indent_str}text '{text_str}':")
         self.indent_level_up()
@@ -852,6 +1048,7 @@ def generate_text_displayable(self, fgui_text):
                     text_code.append(f"{self.indent_str}kerning {fgui_text.letter_spacing}")
                 if fgui_text.leading:
                     text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}")
+                text_code.append(f"{self.indent_str}min_width {fgui_text.size[0]}")
                 xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
                 text_code.append(f"{self.indent_str}textalign {xalign}")
                 if fgui_text.bold:
@@ -888,6 +1085,8 @@ def generate_text_displayable(self, fgui_text):
         if fgui_text.leading:
             text_code.append(f"{self.indent_str}line_leading {fgui_text.leading}")
 
+        #设置最小宽度才能使text_align生效
+        text_code.append(f"{self.indent_str}min_width {fgui_text.size[0]}")
         # Ren'Py中只有文本宽度小于组件宽度的水平方向对齐设置
         xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
         text_code.append(f"{self.indent_str}textalign {xalign}")
@@ -1055,7 +1254,7 @@ def generate_renpy_code(self):
             elif component.extention == 'Label':
                 pass
             elif component.extention == 'Slider':
-                pass
+                self.generate_slider_style(component)
             elif component.extention == 'ComboBox':
                 pass
             elif component.extention == 'ProgressBar':
@@ -1064,6 +1263,8 @@ def generate_renpy_code(self):
                 self.generate_screen(component)
                 self.renpy_code.extend(self.screen_code)
 
+        self.renpy_code.extend(self.style_code)
+
     def save_to_file(self, filename):
         """
         保存Ren'Py代码

From a8d78a9219179c9558bf4fda6b96b4258e6a9967 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sun, 9 Nov 2025 11:48:44 +0800
Subject: [PATCH 10/40] Support sliders in screen

---
 .../utils/renpy/Fgui2RenpyConverter.py               | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index f64a425..fe1ff94 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -359,6 +359,8 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
         style_definition_code.append(f"{self.indent_unit}thumb Fixed(Frame('{fgui_slider.name}_[prefix_]thumb_image',xysize={grip_com.size},pos=({thumb_xpos},{thumb_ypos})))")
         style_definition_code.append("")
 
+        # 添加头部注释
+        slider_style_code.append("# 滑动条样式定义")
         slider_style_code.extend(bar_image_definition_code)
         slider_style_code.extend(style_definition_code)
 
@@ -460,6 +462,16 @@ def generate_screen(self, component):
                         parameter_str = self.generate_button_parameter(None, displayable.custom_data)
                     self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'")
                     self.indent_level_down()
+                    continue
+                # 滑动条
+                if ref_com.extention == "Slider" and ref_com.name != None:
+                    self.screen_ui_code.append(f"{self.indent_str}fixed:")
+                    self.indent_level_up()
+                    self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    bar_value = displayable.custom_data if displayable.custom_data else displayable.slider_property.current_value
+                    self.screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{screen_name}_{displayable.id}'")
+                    self.indent_level_down()
+                    continue
 
         self.screen_code.extend(self.screen_definition_head)
         if self.screen_variable_code:

From 33634fc461b682100ee9877014dadbce17f9a4d8 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 11 Nov 2025 16:01:30 +0800
Subject: [PATCH 11/40] Add special screens (choice, say) converting method.

---
 src/fgui_converter/FguiAssetsParseLib.py      |  16 +-
 .../utils/renpy/Fgui2RenpyConverter.py        | 216 +++++++++++++++++-
 .../renpy_ellipseAA_template.txt              |   2 +-
 .../renpy_ellipse_template.txt                |   2 +-
 .../renpy_rectangleAA_template.txt            |   2 +-
 .../renpy_rectangle_template.txt              |   2 +-
 6 files changed, 223 insertions(+), 17 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 7be9ef2..c12f46b 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -366,6 +366,7 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
 class FguiDisplayList:
     """
     FairyGUI组件内部显示列表，xml中displayList标签内容。
+    只要一个组件不为空组件，必定会有displayList。
     """
     def __init__(self, display_list_etree, package_desc_id=None):
         self.display_list_etree = display_list_etree
@@ -665,12 +666,25 @@ def __init__(self, display_item_tree):
 class FguiImage(FguiDisplayable):
     """
     FairyGUI中的图片。
-    自身没有什么独特属性。
+    tag为image。
+    属性如下：
+    color-颜色：一个6位Hex字符串，表示显示时所有像素的RGA都要乘以该值
+    flip-翻转类型："hz"-水平、"vt"-垂直、"both"-水平+垂直。
+    fillMethod-填充方式："hz"-水平、"vt"-垂直、"radial90"-90度、"radial180"-180度、"radial360"-360度
+    fillOrigin-填充原点：0(默认值)、1、2、3。该值根据不同的填充方式有不同的含义。
+    fillAmount-填充比例：100(默认值)，一个介于0到100之间的整数。
+    样例：
+    <image id="n9_z1bv" name="n9" src="ndicp" xy="210,169" alpha="0.67" color="#000000" flip="both" fillMethod="hz" fillOrigin="1" fillAmount="55"/>
     """
     def __init__(self, display_item_tree):
         if display_item_tree.tag != "image" :
             raise ValueError("xml tag is not image.")
         super().__init__(display_item_tree)
+        self.multiply_color = self.display_item_tree.get("color", "#ffffff")
+        self.flip_type = self.display_item_tree.get("flip")
+        self.fill_method = self.display_item_tree.get("fillMethod")
+        self.fill_origin = self.display_item_tree.get("fillOrigin")
+        self.fill_amount = self.display_item_tree.get("fillAmount")
 
 class FguiListItem():
     """
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index fe1ff94..6d74103 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -9,7 +9,6 @@
 
 import shutil
 from enum import IntEnum
-from tkinter import HORIZONTAL, NO, VERTICAL
 
 # 复合型组件的子组件枚举类型
 class DisplayableChildType(IntEnum):
@@ -51,7 +50,11 @@ class FguiToRenpyConverter:
     # 模态类界面名称，需要添加 Modal True。
     modal_screen_name_list = ['confirm']
 
+    # 选项分支界面。
+    choice_screen_name_list = ['choice']
 
+    # 对话界面
+    say_screen_name_list = ['say']
     
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
@@ -360,8 +363,8 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
         style_definition_code.append("")
 
         # 添加头部注释
-        slider_style_code.append("# 滑动条样式定义")
         slider_style_code.extend(bar_image_definition_code)
+        slider_style_code.append("# 滑动条样式定义")
         slider_style_code.extend(style_definition_code)
 
         self.style_code.extend(slider_style_code)
@@ -375,6 +378,14 @@ def is_menu_screen(screen_name):
     def is_modal_screen(screen_name):
         return screen_name in FguiToRenpyConverter.modal_screen_name_list
 
+    @staticmethod
+    def is_choice_screen(screen_name):
+        return screen_name in FguiToRenpyConverter.choice_screen_name_list
+
+    @staticmethod
+    def is_say_screen(screen_name):
+        return screen_name in FguiToRenpyConverter.say_screen_name_list
+
     def generate_screen(self, component):
         """
         生成screen定义。目标样例：
@@ -410,7 +421,7 @@ def generate_screen(self, component):
         self.screen_has_dismiss = False
         self.dismiss_action_list.clear()
 
-        self.screen_definition_head.append("# 组件Screen")
+        self.screen_definition_head.append("# 界面定义")
         self.screen_definition_head.append(f"# 从FairyGUI组件{component.name}转换而来")
 
         id = component.id
@@ -418,6 +429,16 @@ def generate_screen(self, component):
 
         # 界面入参列表
         screen_params = ''
+        # choice界面的特殊处理
+        if self.is_choice_screen(screen_name):
+            self.generate_choice_screen(component)
+            return
+
+        # say界面的特殊处理
+        if self.is_say_screen(screen_name):
+            self.generate_say_screen(component)
+            return
+
         # confirm 界面固定入参
         if screen_name == 'confirm':
             screen_params = 'message, yes_action, no_action'
@@ -468,7 +489,14 @@ def generate_screen(self, component):
                     self.screen_ui_code.append(f"{self.indent_str}fixed:")
                     self.indent_level_up()
                     self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
-                    bar_value = displayable.custom_data if displayable.custom_data else displayable.slider_property.current_value
+                    # 若在自定义数据中指定了关联数据对象，则直接使用。
+                    if displayable.custom_data:
+                        bar_value = displayable.custom_data
+                    # 若未指定则在screen中生成一个临时变量
+                    else:
+                        variable_name = f"{screen_name}_{displayable.name}_barvalue"
+                        self.screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}")
+                        bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value)
                     self.screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{screen_name}_{displayable.id}'")
                     self.indent_level_down()
                     continue
@@ -491,9 +519,164 @@ def generate_screen(self, component):
         self.screen_ui_code.append("")
         self.screen_code.extend(self.screen_ui_code)
 
+    def generate_choice_screen(self, component):
+        print("This is choice screen.")
+        caption_text = None
+        choice_button = None
+        choice_list = None
+
+        for displayable in component.display_list.displayable_list:
+            # 生成标题文本样式
+            if displayable.name == 'caption' and isinstance(displayable, FguiText):
+                self.generate_text_style(displayable, "choice_caption_text_style")
+                caption_text = displayable
+            # 查找第一个列表
+            if isinstance(displayable, FguiList) and choice_list == None:
+                choice_list = displayable
+                choice_button = self.fgui_assets.get_component_by_id(displayable.default_item_id)
+
+        # 检查查找结果
+        if choice_list == None:
+            print("Lack of choice button list.")
+            return
+        if not isinstance(choice_button, FguiButton):
+            print("Choice button list's item is not Button.")
+            return
+
+        # vbox的行距
+        vbox_spacing = choice_list.line_gap if choice_list else 0
+        screen_params = 'items'
+        self.reset_indent_level()
+        self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
+        self.indent_level_up()
+        # 选项菜单标题
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {caption_text.xypos}")
+        self.screen_ui_code.append(f"{self.indent_str}for i in items:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}if not i.action:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}text i.caption style 'choice_caption_text_style'")
+        self.indent_level_down(3)
+        # 选项菜单按钮列表
+        self.screen_ui_code.append(f"{self.indent_str}vbox:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}spacing {vbox_spacing}")
+        self.screen_ui_code.append(f"{self.indent_str}pos {choice_list.xypos}")
+        self.screen_ui_code.append(f"{self.indent_str}for i in items:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}if i.action:")
+        self.indent_level_up()
+        parameter_str = f"title=i.caption, actions=i.action"
+        self.screen_ui_code.append(f"{self.indent_str}use {choice_button.name}({parameter_str})")
+        self.reset_indent_level()
+
+        self.screen_ui_code.append("")
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_ui_code)
+        return
 
+    def generate_say_screen(self, component):
+        print("This is say screen.")
+        who_text = None
+        what_text = None
+        namebox = None
+        textbox = None
+        namebox_image = 'Null()'
+        textbox_image = 'Null()'
+        namebox_pos = (0, 0)
+        textbox_pos = (0, 0)
 
-    def generate_button_parameter(self, button_title=None, original_actions_str=None):
+        for displayable in component.display_list.displayable_list:
+            # 生成发言角色名的文本样式
+            if displayable.name == 'who' and isinstance(displayable, FguiText):
+                self.generate_text_style(displayable, "say_who_text_style")
+                who_text = displayable
+            # 生成发言内容的文本样式
+            if displayable.name == 'what' and isinstance(displayable, FguiText):
+                self.generate_text_style(displayable, "say_what_text_style")
+                what_text = displayable
+            # 角色名的背景
+            if displayable.name == 'namebox' and isinstance(displayable, FguiImage):
+                namebox = self.fgui_assets.get_component_by_id(displayable.src)
+                namebox_pos = displayable.xypos
+                namebox_image = f"'{self.get_image_name(displayable)}'"
+
+            # 发言内容的背景
+            if displayable.name == 'textbox' and isinstance(displayable, FguiImage):
+                textbox = self.fgui_assets.get_component_by_id(displayable.src)
+                textbox_pos = displayable.xypos
+                textbox_image = f"'{self.get_image_name(displayable)}'"
+
+        # 检查查找结果
+        if who_text == None:
+            print("Lack of who text component.")
+            return
+        if what_text == None:
+            print("Lack of what text component.")
+            return
+        if namebox:
+            print("Namebox background is Null.")
+        if textbox:
+            print("Textbox background is Null.")
+
+        screen_params = 'who, what'
+        self.reset_indent_level()
+        # say界面需要覆盖默认gui设置
+        self.screen_definition_head.append("# say界面")
+        self.screen_definition_head.append("style say_label is say_who_text_style")
+        self.screen_definition_head.append("style say_dialogue is say_what_text_style")
+        self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}if who is not None:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}add {namebox_image}:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {namebox_pos}")
+        self.indent_level_down()
+        self.screen_ui_code.append(f"{self.indent_str}text who id 'who' style 'say_who_text_style':")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {who_text.xypos}")
+        self.indent_level_down(2)
+        self.screen_ui_code.append(f"{self.indent_str}add {textbox_image}:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {textbox_pos}")
+        self.indent_level_down()
+        self.screen_ui_code.append(f"{self.indent_str}text what id 'what' style 'say_what_text_style':")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {what_text.xypos}")
+        self.reset_indent_level()
+
+        self.screen_ui_code.append("")
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_ui_code)
+        return
+
+    # 根据图片组件对象获取图片名
+    def get_image_name(self, fgui_image):
+        if not isinstance(fgui_image, FguiImage):
+            print("It is not a Image object.")
+            return None
+        for image in self.fgui_assets.package_desc.image_list:
+            if fgui_image.src == image.id:
+                return image.name
+
+    @staticmethod
+    def generate_variable_definition_str(variable_name, current_value=None):
+        return f"default {variable_name} = {current_value}"
+
+    @staticmethod
+    def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100, current_value=0, scope='local'):
+        barvalue_str = ''
+        barvalue_scope_str = ''
+        if scope in ('local', 'screen'):
+            barvalue_scope_str = scope.capitalize()
+        barvalue_str = f"{barvalue_scope_str}VariableValue('{barvalue_name}',min={min_value},max={max_value})"
+        return barvalue_str
+
+    @staticmethod
+    def generate_button_parameter(button_title=None, original_actions_str=None):
         parameter_str = ""
         title_str = ""
         actions_str = ""
@@ -529,7 +712,7 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str):
             return
         # 样式具有固定一档的缩进
         style_indent = "    "
-        # default_title = fgui_text.text
+        self.style_code.append(f"# 文本{fgui_text.name}样式定义")
         # 定义样式
         self.style_code.append(f"style {style_name}:")
         self.style_code.append(f"{style_indent}xysize {fgui_text.size}")
@@ -542,8 +725,8 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str):
         self.style_code.append(f"{style_indent}font '{text_font}'")
         self.style_code.append(f"{style_indent}size {fgui_text.font_size}")
         self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'")
-        xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
-        self.style_code.append(f"{style_indent}align ({xalign}, {yalign})")
+        # xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
+        # self.style_code.append(f"{style_indent}align ({xalign}, {yalign})")
 
         text_outline_string = self.generate_text_outline_string(fgui_text)
         self.style_code.append(f"{style_indent}outlines {text_outline_string}")
@@ -669,7 +852,7 @@ def generate_button_screen(self, component):
         self.screen_ui_code.clear()
         self.has_dismiss = False
         self.dismiss_action_list.clear()
-        # self.style_code.clear()
+        self.style_code.clear()
 
         # 4种状态的子组件列表
         idle_child_list = []
@@ -719,7 +902,7 @@ def generate_button_screen(self, component):
             return
 
         # 生成按钮组件screen
-        self.screen_definition_head.append("# 按钮组件Screen")
+        self.screen_definition_head.append("# 按钮screen定义")
         self.screen_definition_head.append(f"# 从FairyGUI按钮组件{component.name}转换而来")
 
         id = component.id
@@ -728,6 +911,8 @@ def generate_button_screen(self, component):
         background = self.default_background
 
         default_title = ''
+        title_displayable = None
+        text_xalign, text_yalign = 0, 0
 
         for displayable in component.display_list.displayable_list:
             # 不带显示控制器表示始终显示
@@ -761,9 +946,10 @@ def generate_button_screen(self, component):
                 # 重置缩进级别
                 self.reset_indent_level()
                 default_title = displayable.text
+                title_displayable = displayable
+                text_xalign, text_yalign = self.trans_text_align(displayable.align, displayable.v_align)
                 # 定义样式
                 self.generate_text_style(displayable, f"{button_name}_text")
-                # self.screen_code.extend(self.style_code)
 
         # 根据state_children_dict生成各种对应状态的background
         # idle_background
@@ -802,7 +988,11 @@ def generate_button_screen(self, component):
         #     self.screen_ui_code.append(f"{self.indent_str}selected_hover_background '{button_name}_selected_hover_background'")
         # if state_children_dict['insensitive']:
         #     self.screen_ui_code.append(f"{self.indent_str}insensitive_background '{button_name}_insensitive_background'")
-        self.screen_ui_code.append(f"{self.indent_str}text title")
+        self.screen_ui_code.append(f"{self.indent_str}text title:")
+        # Ren'Py中没有文本相对自身组件的垂直对齐方式，尝试用整个文本组件的对齐来凑合。
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}align ({text_xalign},{text_yalign})")
+        self.indent_level_down()
         self.screen_ui_code.append(f"{self.indent_str}action actions")
         # 一些按钮特性
         # focus_mask，对应FGUI中的点击“测试”。
@@ -947,6 +1137,8 @@ def generate_image_displayable(self, fgui_image):
                     image_code.append(f"{self.indent_str}rotate {fgui_image.rotation}")
                 if fgui_image.alpha != 1.0:
                     image_code.append(f"{self.indent_str}alpha {fgui_image.alpha}")
+                if fgui_image.multiply_color != "#ffffff":
+                    image_code.append(f"{self.indent_str}matrixcolor TintMatrix('{fgui_image.multiply_color}')")
                 if fgui_image.scale != (1.0, 1.0):
                     image_code.append(f"{self.indent_str}xzoom {fgui_image.scale[0]} yzoom {fgui_image.scale[1]}")
                 # 九宫格或平铺图片需要指定尺寸
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
index cd6a884..6777d81 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
@@ -1,4 +1,4 @@
-# 抗锯齿的椭圆(圆形)图像定义
+# 抗锯齿的椭圆(圆形)图形定义
 image {image_name}:
     Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01)
     xysize {xysize}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
index 364e171..25aaa92 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
@@ -1,4 +1,4 @@
-# 椭圆(圆形)图像定义
+# 椭圆(圆形)图形定义
 image {image_name}:
     Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness})
     xysize {xysize}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
index 75237be..f795118 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
@@ -1,4 +1,4 @@
-# 带光滑渐变的圆角矩形图像定义
+# 带光滑渐变的圆角矩形图形定义
 image {image_name}:
     Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0)
     xysize {xysize}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
index eb60024..b58d746 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
@@ -1,4 +1,4 @@
-# 圆角矩形图像定义
+# 圆角矩形图形定义
 image {image_name}:
     Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness})
     xysize {xysize}

From 82b2e345641dbf7d9019e4f8ac078530c7b4e354 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Wed, 12 Nov 2025 00:07:23 +0800
Subject: [PATCH 12/40] Some component, as button and slider, could get custom
 data from source component configure.

---
 src/fgui_converter/FguiAssetsParseLib.py        |  2 ++
 .../utils/renpy/Fgui2RenpyConverter.py          | 17 +++++++++++++----
 2 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index c12f46b..dfe8897 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -263,6 +263,8 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
         # 点击测试区域，包含3个字段，分别为引用源src和组件内相对坐标x、y。
         hit_test = component_etree.get("hitTest")
         self.hit_test = FguiHitTest(hit_test) if hit_test else None
+        # 自定义数据。实际使用时，相同id的FguiDisplayable中的自定义数据优先。
+        self.custom_data = component_etree.get("customData")
 
         # 控制器，一般不超过1个。
         self.controller_list = []
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 6d74103..047f86c 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -45,7 +45,7 @@ class FguiToRenpyConverter:
     default_background = '#fff'
 
     # 菜单类界面名称，需要添加 tag menu。
-    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences']
+    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about']
 
     # 模态类界面名称，需要添加 Modal True。
     modal_screen_name_list = ['confirm']
@@ -476,11 +476,13 @@ def generate_screen(self, component):
                     self.screen_ui_code.append(f"{self.indent_str}fixed:")
                     self.indent_level_up()
                     self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。
+                    actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data
                     # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
                     if displayable.button_property:
-                        parameter_str = self.generate_button_parameter(displayable.button_property.title, displayable.custom_data)
+                        parameter_str = self.generate_button_parameter(displayable.button_property.title, actions)
                     else:
-                        parameter_str = self.generate_button_parameter(None, displayable.custom_data)
+                        parameter_str = self.generate_button_parameter(None, actions)
                     self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'")
                     self.indent_level_down()
                     continue
@@ -492,6 +494,9 @@ def generate_screen(self, component):
                     # 若在自定义数据中指定了关联数据对象，则直接使用。
                     if displayable.custom_data:
                         bar_value = displayable.custom_data
+                    # 否则再查找引用源对象的自定义数据
+                    elif ref_com.custom_data:
+                        bar_value = ref_com.custom_data
                     # 若未指定则在screen中生成一个临时变量
                     else:
                         variable_name = f"{screen_name}_{displayable.name}_barvalue"
@@ -966,7 +971,8 @@ def generate_button_screen(self, component):
 
         # 重置缩进级别
         self.reset_indent_level()
-        self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions=NullAction()):")
+        default_actions = component.custom_data if component.custom_data else 'NullAction()'
+        self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}):")
         self.indent_level_up()
         # 如果按钮有按下效果，添加自定义组件
         if component.button_down_effect:
@@ -1040,6 +1046,9 @@ def generate_image_object(self, image_name : str, displayable_list : list, compo
             # 其他类型暂时用空对象占位
             else:
                 image_definitions.append(f"{self.indent_str}Null()")
+            image_definitions.append(f"{self.indent_str}pos {displayable.xypos}")
+            # TODO 其他transform property还待添加
+            # 也可考虑检查displayList中的子组件，将非默认值值生成在一个额外的容器中，便于用循环生成脚本。
             self.indent_level_down()
 
         image_definitions.append("")

From 616a52cee936672226c9e1dcf3166237062e1de2 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Wed, 12 Nov 2025 23:02:00 +0800
Subject: [PATCH 13/40] Add special screens (say, save, load) converting
 method.

1. Optimized screen generating method.
2. Add special screens converting.
3. Fixed rectangle shader transparent issue.
---
 .../utils/renpy/Fgui2RenpyConverter.py        | 399 ++++++++++++++----
 .../renpy/renpy_templates/02_renpy_shader.rpy |   8 +-
 2 files changed, 317 insertions(+), 90 deletions(-)

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 047f86c..fc460d7 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -55,6 +55,9 @@ class FguiToRenpyConverter:
 
     # 对话界面
     say_screen_name_list = ['say']
+
+    # 存档相关界面
+    save_load_screen_name_list = ['save', 'load']
     
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
@@ -65,6 +68,9 @@ def __init__(self, fgui_assets):
         self.screen_function_code = []
         self.screen_ui_code = []
         self.style_code = []
+        self.image_definition_code = []
+        self.graph_definition_code = []
+        self.game_global_variables_code = []
         
         # dismiss用于取消一些组件的focus状态，例如input。
         self.screen_has_dismiss = False
@@ -88,6 +94,11 @@ def __init__(self, fgui_assets):
         self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt')
         self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt')
 
+    def set_game_global_variables(self, variable_name, variable_value):
+        variable_str = f"define {variable_name} = {variable_value}"
+        self.game_global_variables_code.append(variable_str)
+        self.game_global_variables_code.append('')
+
     def calculate_indent(self):
         self.indent_str = self.indent_unit * self.root_indent_level
         return self.indent_str
@@ -158,7 +169,7 @@ def generate_image_definitions(self):
                     image_definitions.append(f'image {image_name} = im.Crop("{atlas_file}", ({x}, {y}, {width}, {height}))')
 
         image_definitions.append("")
-        self.renpy_code.extend(image_definitions)
+        self.image_definition_code.extend(image_definitions)
 
     def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : str) -> list:
         """
@@ -371,22 +382,95 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
 
 
     @staticmethod
-    def is_menu_screen(screen_name):
+    def is_menu_screen(screen_name : str):
         return screen_name in FguiToRenpyConverter.menu_screen_name_list
     
     @staticmethod
-    def is_modal_screen(screen_name):
+    def is_modal_screen(screen_name : str):
         return screen_name in FguiToRenpyConverter.modal_screen_name_list
 
     @staticmethod
-    def is_choice_screen(screen_name):
+    def is_choice_screen(screen_name : str):
         return screen_name in FguiToRenpyConverter.choice_screen_name_list
 
     @staticmethod
-    def is_say_screen(screen_name):
+    def is_say_screen(screen_name : str):
         return screen_name in FguiToRenpyConverter.say_screen_name_list
 
-    def generate_screen(self, component):
+    @staticmethod
+    def is_save_load_screen(screen_name : str):
+        return screen_name in FguiToRenpyConverter.save_load_screen_name_list
+
+    def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1):
+        screen_ui_code = []
+        # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}")
+        end_index = len(component.display_list.displayable_list) if (list_end_index == -1) else list_end_index
+        for displayable in component.display_list.displayable_list[list_begin_index:end_index]:
+            # print(displayable.name)
+            # 图片组件
+            if isinstance(displayable, FguiImage):
+                screen_ui_code.extend(self.generate_image_displayable(displayable))
+            # 图形组件
+            elif isinstance(displayable, FguiGraph):
+                screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name))
+            # 文本组件
+            elif isinstance(displayable, FguiText):
+                screen_ui_code.extend(self.generate_text_displayable(displayable))
+            # 列表
+            elif isinstance(displayable, FguiList):
+                screen_ui_code.extend(self.generate_list_displayable(displayable))
+            # 装载器
+            elif isinstance(displayable, FguiLoader):
+                pass
+            # 其他组件
+            else:
+                # 根据引用源id查找组件
+                ref_com = self.fgui_assets.get_component_by_id(displayable.src)
+                # 按钮。可设置标题，并根据自定义数据字段设置action。
+                if ref_com.extention == "Button" and ref_com.name != None:
+                    screen_ui_code.append(f"{self.indent_str}fixed:")
+                    self.indent_level_up()
+                    screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。
+                    actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data
+                    # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
+                    if displayable.button_property:
+                        parameter_str = self.generate_button_parameter(displayable.button_property.title, actions)
+                    else:
+                        parameter_str = self.generate_button_parameter(None, actions)
+                    screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{component.name}_{displayable.id}'")
+                    self.indent_level_down()
+                    continue
+                # 滑动条
+                if ref_com.extention == "Slider" and ref_com.name != None:
+                    screen_ui_code.append(f"{self.indent_str}fixed:")
+                    self.indent_level_up()
+                    screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    # 若在自定义数据中指定了关联数据对象，则直接使用。
+                    if displayable.custom_data:
+                        bar_value = displayable.custom_data
+                    # 否则再查找引用源对象的自定义数据
+                    elif ref_com.custom_data:
+                        bar_value = ref_com.custom_data
+                    # 若未指定则在screen中生成一个临时变量
+                    else:
+                        variable_name = f"{component.name}_{displayable.name}_barvalue"
+                        screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}")
+                        bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value)
+                    screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'")
+                    self.indent_level_down()
+                    continue
+                # 其他组件
+                screen_ui_code.append(f"{self.indent_str}fixed:")
+                self.indent_level_up()
+                screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'")
+                self.indent_level_down()
+        # print("convert_component_display_list method:")
+        # print(screen_ui_code)
+        return screen_ui_code
+
+    def generate_screen(self, component : FguiComponent):
         """
         生成screen定义。目标样例：
 
@@ -413,7 +497,7 @@ def generate_screen(self, component):
                 pos (1007, 483)
                 use main_menu_button(title='放弃', actions=Quit())
         """
-        self.screen_code.clear()
+        # self.screen_code.clear()
         self.screen_definition_head.clear()
         self.screen_variable_code.clear()
         self.screen_function_code.clear()
@@ -439,6 +523,11 @@ def generate_screen(self, component):
             self.generate_say_screen(component)
             return
 
+        # save和load界面的特殊处理
+        if self.is_save_load_screen(screen_name):
+            self.generate_save_load_screen(component)
+            return
+
         # confirm 界面固定入参
         if screen_name == 'confirm':
             screen_params = 'message, yes_action, no_action'
@@ -448,63 +537,13 @@ def generate_screen(self, component):
         self.indent_level_up()
         if self.is_menu_screen(screen_name):
             self.screen_ui_code.append(f"{self.indent_str}tag menu\n")
+            # 若自定义了game_menu，则修改默认游戏内菜单显示的控制变量。
+            if screen_name == "game_menu":
+                self.set_game_global_variables('_game_menu_screen', str("\"game_menu\""))
         if self.is_modal_screen(screen_name):
             self.screen_ui_code.append(f"{self.indent_str}modal True")
             self.screen_ui_code.append(f"{self.indent_str}zorder 200\n")
-        for displayable in component.display_list.displayable_list:
-            # 图片组件
-            if isinstance(displayable, FguiImage):
-                self.screen_ui_code.extend(self.generate_image_displayable(displayable))
-            # 图形组件
-            elif isinstance(displayable, FguiGraph):
-                self.screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name))
-            # 文本组件
-            elif isinstance(displayable, FguiText):
-                self.screen_ui_code.extend(self.generate_text_displayable(displayable))
-            # 列表
-            elif isinstance(displayable, FguiList):
-                self.screen_ui_code.extend(self.generate_list_displayable(displayable))
-            # 装载器
-            elif isinstance(displayable, FguiLoader):
-                pass
-            # 其他组件
-            else:
-                # 根据引用源id查找组件
-                ref_com = self.fgui_assets.get_component_by_id(displayable.src)
-                # 按钮。可设置标题，并根据自定义数据字段设置action。
-                if ref_com.extention == "Button" and ref_com.name != None:
-                    self.screen_ui_code.append(f"{self.indent_str}fixed:")
-                    self.indent_level_up()
-                    self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
-                    # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。
-                    actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data
-                    # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
-                    if displayable.button_property:
-                        parameter_str = self.generate_button_parameter(displayable.button_property.title, actions)
-                    else:
-                        parameter_str = self.generate_button_parameter(None, actions)
-                    self.screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{screen_name}_{displayable.id}'")
-                    self.indent_level_down()
-                    continue
-                # 滑动条
-                if ref_com.extention == "Slider" and ref_com.name != None:
-                    self.screen_ui_code.append(f"{self.indent_str}fixed:")
-                    self.indent_level_up()
-                    self.screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
-                    # 若在自定义数据中指定了关联数据对象，则直接使用。
-                    if displayable.custom_data:
-                        bar_value = displayable.custom_data
-                    # 否则再查找引用源对象的自定义数据
-                    elif ref_com.custom_data:
-                        bar_value = ref_com.custom_data
-                    # 若未指定则在screen中生成一个临时变量
-                    else:
-                        variable_name = f"{screen_name}_{displayable.name}_barvalue"
-                        self.screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}")
-                        bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value)
-                    self.screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{screen_name}_{displayable.id}'")
-                    self.indent_level_down()
-                    continue
+        self.screen_ui_code.extend(self.convert_component_display_list(component))
 
         self.screen_code.extend(self.screen_definition_head)
         if self.screen_variable_code:
@@ -524,8 +563,14 @@ def generate_screen(self, component):
         self.screen_ui_code.append("")
         self.screen_code.extend(self.screen_ui_code)
 
-    def generate_choice_screen(self, component):
+    def generate_choice_screen(self, component : FguiComponent):
         print("This is choice screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
         caption_text = None
         choice_button = None
         choice_list = None
@@ -582,8 +627,142 @@ def generate_choice_screen(self, component):
         self.screen_code.extend(self.screen_ui_code)
         return
 
-    def generate_say_screen(self, component):
+    def generate_save_load_screen(self, component : FguiComponent):
+        print(f"This is {component.name} screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+        slot_list = None
+        slot_list_index = -1
+        default_slot_button = None
+        if component.display_list.displayable_list == None:
+            print(f"{component.name} contains no displayable.")
+            return
+        for i in range(len(component.display_list.displayable_list)):
+            displayable = component.display_list.displayable_list[i]
+            # 搜索名为 save_slot_list 的列表组件
+            if displayable.name == 'save_slot_list' and isinstance(displayable, FguiList):
+                slot_list = displayable
+                slot_list_index = i
+                # 确认默认引用组件类型。
+                default_slot_button = self.fgui_assets.get_component_by_id(slot_list.default_item_id)
+                break
+        if not slot_list:
+            print(f"{component.name} contains no slot list.")
+            return
+        # 检查slot_list默认引用组件类型是否为button
+        if not isinstance(default_slot_button, FguiButton):
+            print(f"{component.name} slot list item is not Button.")
+            return
+        
+        self.screen_definition_head.append("# 存档/读档 界面")
+        self.screen_definition_head.append(f"screen {component.name}():")
+        # save_slot_list 之前的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=slot_list_index))
+
+        # save_slot_list的处理
+        slot_list_code = []
+        item_number = len(slot_list.item_list)
+
+        # 根据“溢出处理”是否可见区分处理。
+        # 若“可见”，则使用hbox、vbox和grid。
+        if slot_list.overflow == "visible":
+            # 单列竖排，使用vbox
+            if slot_list.layout == "column":
+                slot_list_code.append(f"{self.indent_str}vbox:")
+            # 单行横排，使用hbox
+            elif slot_list.layout == "row":
+                slot_list_code.append(f"{self.indent_str}hbox:")
+            # 其他，使用grid
+            else:
+                slot_list_code.append(f"{self.indent_str}grid {slot_list.line_item_count} {slot_list.line_item_count2}:")
+
+        else:
+            slot_list_code.append(f"{self.indent_str}vpgrid:")
+            self.indent_level_up()
+
+            # 若“隐藏”，使用不可滚动的vpgrid
+            if slot_list.overflow == "hidden":
+                slot_list_code.append(f"{self.indent_str}draggable False")
+                # 单列竖排
+                if slot_list.layout == "column":
+                    cols = 1
+                    rows = item_number
+                # 单行横排
+                elif slot_list.layout == "row":
+                    cols = item_number
+                    rows = 1
+                # 其他
+                else:
+                    cols = slot_list.line_item_count
+                    rows = slot_list.line_item_count2
+                slot_list_code.append(f"{self.indent_str}cols {cols}")
+                slot_list_code.append(f"{self.indent_str}rows {rows}")
+            # 若“滚动”，使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。
+            elif slot_list.overflow == "scroll":
+                slot_list_code.append(f"{self.indent_str}draggable True")
+                # 垂直滚动
+                if slot_list.scroll == "vertical":
+                    pass
+                # 水平滚动
+                elif slot_list.scroll == "horizontal":
+                    pass
+                # 自由滚动
+                elif slot_list.scroll == "both":
+                    pass
+                # 单列竖排，使用vbox
+                if slot_list.layout == "column":
+                    cols = 1
+                    rows = item_number
+                # 单行横排，使用hbox
+                elif slot_list.layout == "row":
+                    cols = item_number
+                    rows = 1
+                # 其他，使用grid
+                else:
+                    cols = slot_list.line_item_count
+                    rows = slot_list.line_item_count2
+                slot_list_code.append(f"{self.indent_str}cols {cols}")
+                slot_list_code.append(f"{self.indent_str}rows {rows}")
+                if slot_list.line_gap:
+                    slot_list_code.append(f"{self.indent_str}yspacing {slot_list.line_gap}")
+                if slot_list.col_gap:
+                    slot_list_code.append(f"{self.indent_str}xspacing {slot_list.col_gap}")
+            self.indent_level_down()
+
+        self.indent_level_up()
+        slot_list_code.append(f"{self.indent_str}pos {slot_list.xypos}")
+        slot_list_code.append(f"{self.indent_str}xysize {slot_list.size}")
+        if slot_list.margin:
+            slot_list_code.append(f"{self.indent_str}margin {slot_list.margin}")
+        # 添加元素
+        for i in range(item_number):
+            # 值根据列表长度添加对应数量的默认元素，即存档按钮
+            parameter_str = self.generate_button_callable_parameter(f"FileTime({i+1}, format=_(\"{{#file_time}}%Y-%m-%d %H:%M\"), empty=_(''))", f"FileAction({i+1})", f"FileScreenshot({i+1})")
+            slot_list_code.append(f"{self.indent_str}use {default_slot_button.name}({parameter_str})")
+
+        self.indent_level_down()
+        self.screen_ui_code.extend(slot_list_code)
+
+        # save_slot_list 之后的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=slot_list_index+1))
+        self.screen_ui_code.append("")
+
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_ui_code)
+        return
+
+    def generate_say_screen(self, component : FguiComponent):
         print("This is say screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
         who_text = None
         what_text = None
         namebox = None
@@ -681,19 +860,35 @@ def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100,
         return barvalue_str
 
     @staticmethod
-    def generate_button_parameter(button_title=None, original_actions_str=None):
+    def generate_button_parameter(button_title=None, original_actions_str=None, icon=None):
         parameter_str = ""
-        title_str = ""
-        actions_str = ""
+        title_str = "title=''"
+        actions_str = "actions=None"
+        icon_str = "icon=Null()"
         if button_title :
-            title_str = f"title=\'{button_title}\'".replace("\n", "\\n").replace("\r", "\\n")
+            title_str = f"title=\"{button_title}\"".replace("\n", "\\n").replace("\r", "\\n")
         if original_actions_str :
             actions_str = f"actions={original_actions_str}"
-        if button_title and original_actions_str:
-            parameter_str = f"{title_str}, {actions_str}"
-        else:
-            parameter_str = title_str if title_str else actions_str
+        if icon:
+            icon_str = f"icon={icon}"
+
+        parameter_str = f"{title_str}, {actions_str}, {icon_str}"
+        return parameter_str
+
+    @staticmethod
+    def generate_button_callable_parameter(button_title=None, actions=None, icon=None):
+        parameter_str = ""
+        title_str = "title=''"
+        actions_str = "actions=None"
+        icon_str = "icon=Null()"
+        if button_title :
+            title_str = f"title={button_title}".replace("\n", "\\n").replace("\r", "\\n")
+        if actions:
+            actions_str = f"actions={actions}"
+        if icon:
+            icon_str = f"icon={icon}"
 
+        parameter_str = f"{title_str}, {actions_str}, {icon_str}"
         return parameter_str
 
     def generate_text_style(self, fgui_text : FguiText, style_name : str):
@@ -834,6 +1029,7 @@ def generate_button_screen(self, component):
 
         screen main_menu_button(title='', actions=NullAction()):
             button:
+                padding (0, 0, 0, 0)
                 xysize (273, 61)
                 style_prefix 'main_menu_button'
                 background 'main_menu_button_bg'
@@ -850,14 +1046,14 @@ def generate_button_screen(self, component):
             textalign 0.5
         """
 
-        self.screen_code.clear()
+        # self.screen_code.clear()
         self.screen_definition_head.clear()
         self.screen_variable_code.clear()
         self.screen_function_code.clear()
         self.screen_ui_code.clear()
         self.has_dismiss = False
         self.dismiss_action_list.clear()
-        self.style_code.clear()
+        # self.style_code.clear()
 
         # 4种状态的子组件列表
         idle_child_list = []
@@ -918,6 +1114,12 @@ def generate_button_screen(self, component):
         default_title = ''
         title_displayable = None
         text_xalign, text_yalign = 0, 0
+        title_pos = (0, 0)
+        title_anchor = (0, 0)
+
+        icon_pos = (0, 0)
+        icon_size = (0, 0)
+        icon_image = None
 
         for displayable in component.display_list.displayable_list:
             # 不带显示控制器表示始终显示
@@ -941,10 +1143,8 @@ def generate_button_screen(self, component):
                     if displayable.src == image.id:
                         image_id_name_mapping[displayable.id] = image.name
                         break
-                # print(image_id_name_mapping)
             elif isinstance(displayable, FguiGraph):
-                self.renpy_code.extend(self.generate_graph_definitions(displayable, button_name))
-                # break
+                self.graph_definition_code.extend(self.generate_graph_definitions(displayable, button_name))
             # 文本组件。只处理名为title的文本组件。其他的文本待后续增加。
             # FGUI与Ren'Py中的相同的文本对齐方式渲染效果略有不同，Ren'Py的效果更好。
             elif isinstance(displayable, FguiText) and displayable.name == 'title':
@@ -953,8 +1153,16 @@ def generate_button_screen(self, component):
                 default_title = displayable.text
                 title_displayable = displayable
                 text_xalign, text_yalign = self.trans_text_align(displayable.align, displayable.v_align)
+                title_pos = displayable.xypos
+                title_anchor = displayable.pivot
                 # 定义样式
                 self.generate_text_style(displayable, f"{button_name}_text")
+            # icon组件仅作为一个可从按钮外部传入额外可视组件的入口。
+            elif isinstance(displayable, FguiLoader) and displayable.name == 'icon':
+                icon_pos = displayable.xypos
+                icon_size = displayable.size
+                icon_image = displayable.item_url
+                pass
 
         # 根据state_children_dict生成各种对应状态的background
         # idle_background
@@ -972,7 +1180,10 @@ def generate_button_screen(self, component):
         # 重置缩进级别
         self.reset_indent_level()
         default_actions = component.custom_data if component.custom_data else 'NullAction()'
-        self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}):")
+        param_str = self.generate_button_parameter(default_title, default_actions, icon_image)
+        self.screen_definition_head.append(f"screen {button_name}({param_str}):")
+        # self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}, icon=Null()):")
+
         self.indent_level_up()
         # 如果按钮有按下效果，添加自定义组件
         if component.button_down_effect:
@@ -981,8 +1192,10 @@ def generate_button_screen(self, component):
             self.screen_ui_code.append(f"{self.indent_str}pressed_{component.button_down_effect} {component.button_down_effect_value}")
         self.screen_ui_code.append(f"{self.indent_str}button:")
         self.indent_level_up()
+        # 默认button样式的padding为(6,6,6,6)，可能导致部分子组件位置变化。
+        self.screen_ui_code.append(f"{self.indent_str}padding (0, 0, 0, 0)")
         self.screen_ui_code.append(f"{self.indent_str}style_prefix '{button_name}'")
-        self.screen_ui_code.append(f"{self.indent_str}xysize ({xysize})")
+        self.screen_ui_code.append(f"{self.indent_str}xysize {xysize}")
         self.screen_ui_code.append(f"{self.indent_str}background '{button_name}_[prefix_]background'")
         # if state_children_dict['idle']:
         #     self.screen_ui_code.append(f"{self.indent_str}idle_background '{button_name}_idle_background'")
@@ -997,9 +1210,17 @@ def generate_button_screen(self, component):
         self.screen_ui_code.append(f"{self.indent_str}text title:")
         # Ren'Py中没有文本相对自身组件的垂直对齐方式，尝试用整个文本组件的对齐来凑合。
         self.indent_level_up()
-        self.screen_ui_code.append(f"{self.indent_str}align ({text_xalign},{text_yalign})")
+        # self.screen_ui_code.append(f"{self.indent_str}align ({text_xalign},{text_yalign})")
+        self.screen_ui_code.append(f"{self.indent_str}pos {title_pos}")
+        self.screen_ui_code.append(f"{self.indent_str}anchor {title_anchor}")
         self.indent_level_down()
         self.screen_ui_code.append(f"{self.indent_str}action actions")
+        # 在最上层加上icon
+        self.screen_ui_code.append(f"{self.indent_str}add icon:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {icon_pos}")
+        self.screen_ui_code.append(f"{self.indent_str}size {icon_size}")
+        self.indent_level_down()
         # 一些按钮特性
         # focus_mask，对应FGUI中的点击“测试”。
         if component.hit_test:
@@ -1009,10 +1230,10 @@ def generate_button_screen(self, component):
 
         self.screen_ui_code.append("")
 
-        self.screen_code.extend(self.style_code)
+        # self.screen_code.extend(self.style_code)
         self.screen_code.extend(self.screen_definition_head)
         self.screen_code.extend(self.screen_ui_code)
-        self.renpy_code.extend(self.screen_code)
+        # self.renpy_code.extend(self.screen_code)
 
     def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"):
         return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5)
@@ -1053,7 +1274,7 @@ def generate_image_object(self, image_name : str, displayable_list : list, compo
 
         image_definitions.append("")
 
-        self.renpy_code.extend(image_definitions)
+        self.image_definition_code.extend(image_definitions)
         self.root_indent_level = indent_level
 
     @staticmethod
@@ -1108,7 +1329,7 @@ def generate_text_displayable_string(self, fgui_text):
         text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_min_width_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})"
         return text_displayable_string
 
-    def generate_image_displayable(self, fgui_image):
+    def generate_image_displayable(self, fgui_image : FguiImage):
         """
         生成图片组件。
         前提为image对象的定义已经在generate_image_definitions中生成。
@@ -1173,7 +1394,7 @@ def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : st
             print("It is not a graph displayable.")
             return graph_code
 
-        self.renpy_code.extend(self.generate_graph_definitions(fgui_graph, component_name))
+        self.graph_definition_code.extend(self.generate_graph_definitions(fgui_graph, component_name))
         graph_code.append(f"{self.indent_str}add '{component_name}_{fgui_graph.id}'")
 
         return graph_code
@@ -1474,9 +1695,12 @@ def generate_renpy_code(self):
                 pass
             else:
                 self.generate_screen(component)
-                self.renpy_code.extend(self.screen_code)
-
+        
+        self.renpy_code.extend(self.game_global_variables_code)
+        self.renpy_code.extend(self.image_definition_code)
+        self.renpy_code.extend(self.graph_definition_code)
         self.renpy_code.extend(self.style_code)
+        self.renpy_code.extend(self.screen_code)
 
     def save_to_file(self, filename):
         """
@@ -1525,6 +1749,9 @@ def cleanup(self):
             self.screen_ui_code.clear()
             self.style_code.clear()
             self.dismiss_action_list.clear()
+            self.graph_definition_code.clear()
+            self.image_definition_code.clear()
+            self.game_global_variables_code.clear()
             
             # 重置缩进相关状态
             self.root_indent_level = 0
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
index c14bf28..2256c87 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
+++ b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
@@ -19,9 +19,9 @@ init python:
         vec2 uv = v_tex_coord - vec2(0.5, 0.5);
         vec2 tex_pos = uv * u_model_size;
         float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius);
-        float border_alpha = (1.0 - step(0.0, out_distance)) * u_stroke_color.a;
+        float border_alpha = (1.0 - step(0.0, out_distance));
         float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius);
-        float fill_alpha = (1.0 - step(0.0, in_distance)) * u_rectangle_color.a;
+        float fill_alpha = (1.0 - step(0.0, in_distance));
         vec4 c1 = step(1-fill_alpha, 0) * u_rectangle_color;
         vec4 c2 = step(fill_alpha, 0) * border_alpha * u_stroke_color;
         gl_FragColor = c1 + c2;
@@ -47,9 +47,9 @@ init python:
         vec2 uv = v_tex_coord - vec2(0.5, 0.5);
         vec2 tex_pos = uv * u_model_size;
         float out_distance = roundedBoxSDF(tex_pos, u_model_size/2, u_radius);
-        float border_alpha = (1.0 - smoothstep(-u_edge_softness, u_edge_softness, out_distance)) * u_stroke_color.a;
+        float border_alpha = (1.0 - smoothstep(-u_edge_softness, u_edge_softness, out_distance));
         float in_distance = roundedBoxSDF(tex_pos, u_model_size/2-vec2(u_thickness,u_thickness), u_radius);
-        float fill_alpha = (1.0 - smoothstep(0, u_edge_softness, in_distance)) * u_rectangle_color.a;
+        float fill_alpha = (1.0 - smoothstep(0, u_edge_softness, in_distance));
         vec4 c1 = fill_alpha * u_rectangle_color;
         vec4 c2 = border_alpha * u_stroke_color;
         gl_FragColor = mix(c2, c1, fill_alpha);

From 00f0ac67f4b96d4823d6df0c9bd8692e5ca835ee Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Thu, 13 Nov 2025 18:51:48 +0800
Subject: [PATCH 14/40] Add special screens (history, gallery) converting
 method.

1. Add special screens converting.
2. Support FairyFGUI sprite description format(7 fields every image) when publishing assets without cropping transparent parts.
3. Optimized list generating method.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  32 +-
 .../utils/renpy/Fgui2RenpyConverter.py        | 317 +++++++++++++++---
 2 files changed, 296 insertions(+), 53 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index dfe8897..9a974a4 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -110,10 +110,11 @@ def TransStrToBoolean(str):
 class FguiSpriteInfo:
     """
     FairyGUI资源包中的Sprite描述内容。
-    总计11个字段，包括image id、图集编号、在图集中的x坐标、在图集中的y坐标、image宽度、image高度、rotate、
-    原图相对image的x偏移、原图相对image的y偏移、原图宽度、原图高度。
+    支持7或11个字段：
+    - 7个字段：image id、图集编号、x、y、width、height、rotate
+    - 11个字段（完整）：再加上offset_x、offset_y、source_width、source_height
     """
-    def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x, offset_y, source_width, source_height):
+    def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x=0, offset_y=0, source_width=None, source_height=None):
         self.image_id = image_id
         self.atlas_index = int(atlas_index)
         self.x = int(x)
@@ -123,8 +124,9 @@ def __init__(self, image_id, atlas_index, x, y, width, height, rotate, offset_x,
         self.rotate = int(rotate)
         self.offset_x = int(offset_x)
         self.offset_y = int(offset_y)
-        self.source_width = int(source_width)
-        self.source_height = int(source_height)
+        # 若缺省原图尺寸，则默认与裁切后尺寸一致
+        self.source_width = int(source_width) if source_width is not None else int(width)
+        self.source_height = int(source_height) if source_height is not None else int(height)
 
     def __repr__(self):
         return f"FguiSpriteInfo({self.image_id}, {self.atlas_index}, {self.x}, {self.y}, {self.width}, {self.height}, {self.rotate}, {self.offset_x}, {self.offset_y}, {self.source_width}, {self.source_height})"
@@ -134,10 +136,14 @@ def ParseFguiSpriteDescFile(sprite_desc_file):
     fgui_image_sets = []
     with open(sprite_desc_file, "r", encoding="utf-8") as f:
         for line in f:
+            if line.startswith("//"):
+                continue
             parts = line.strip().split()
-            if len(parts) == 11:  # 确保每行有11个字段
+            if len(parts) == 11 or len(parts) == 7:  # 支持7或11个字段
                 obj = FguiSpriteInfo(*parts)
                 fgui_image_sets.append(obj)
+            else:
+                raise ValueError("Number of Sprite description's fields is neither 11 nor 7.")
     return fgui_image_sets
 
 # 从xml字符串创建lxml的etree对象
@@ -402,7 +408,7 @@ def hex_aarrggbb_to_rgba(hex_color):
 
     # 检查处理后的字符串长度是否为8或6
     if hex_str_len != 8 and hex_str_len != 6:
-        raise ValueError("输入的十六进制字符串必须是8位(AARRGGBB)或6位(RRGGBB)")
+        raise ValueError("Color String must be 8 characters(AARRGGBB) or 6 characters(RRGGBB)")
 
     # 6位字符则加上alpha通道的默认值 ff
     if hex_str_len ==6:
@@ -415,7 +421,7 @@ def hex_aarrggbb_to_rgba(hex_color):
         g = int(clean_hex[4:6], 16)
         b = int(clean_hex[6:8], 16)
     except ValueError:
-        raise ValueError("字符串包含无效的十六进制字符")
+        raise ValueError("Color Hex String contains invalid character.")
 
     return (r, g, b, a)
 
@@ -753,8 +759,8 @@ def __init__(self, display_item_tree, package_description_id=None):
         margin = self.display_item_tree.get("margin")
         self.margin = tuple(map(int, margin.split(","))) if margin else None
         self.clip_softness = self.display_item_tree.get("clipSoftness", "0,0")
-        self.line_item_count = int(self.display_item_tree.get("lineItemCount", "1"))
-        self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "1"))
+        self.line_item_count = int(self.display_item_tree.get("lineItemCount", "0"))
+        self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "0"))
         self.line_gap = int(self.display_item_tree.get("lineGap", "0"))
         self.col_gap = int(self.display_item_tree.get("colGap", "0"))
         self.default_item_url = self.display_item_tree.get("defaultItem")
@@ -1069,16 +1075,16 @@ def clear(self):
         self.fgui_image_set.clear()
         self.fgui_atlas_dicts.clear()
 
-    def get_componentname_by_id(self, id):
+    def get_componentname_by_id(self, id : str) -> str:
         return self.package_desc.id_name_mapping[id]
 
-    def get_component_by_id(self, id):
+    def get_component_by_id(self, id : str) -> FguiComponent:
         for component in self.fgui_component_set:
             if component.id == id:
                 return component
         return None
 
-    def get_image_size_by_id(self, id):
+    def get_image_size_by_id(self, id : str):
         for image in self.fgui_image_set:
             if image.image_id == id:
                 return (image.width, image.height)
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index fc460d7..bb28c6a 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -6,7 +6,7 @@
 import os
 import re
 import argparse
-
+import math
 import shutil
 from enum import IntEnum
 
@@ -45,7 +45,7 @@ class FguiToRenpyConverter:
     default_background = '#fff'
 
     # 菜单类界面名称，需要添加 tag menu。
-    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about']
+    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about', 'gallery']
 
     # 模态类界面名称，需要添加 Modal True。
     modal_screen_name_list = ['confirm']
@@ -58,6 +58,10 @@ class FguiToRenpyConverter:
 
     # 存档相关界面
     save_load_screen_name_list = ['save', 'load']
+
+    # 历史界面
+    history_screen_name_list = ['history']
+    history_item_screen_name_list = ['history_item']
     
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
@@ -121,6 +125,9 @@ def generate_image_definitions(self):
         image_definitions.append("# 图像定义")
         image_definitions.append("# 从FairyGUI图集中提取的图像")
 
+        if not self.fgui_assets.fgui_image_set:
+            print("Image set is Null.")
+
         for sprite in self.fgui_assets.fgui_image_set:
             # 找到对应的图像信息
             image_info = None
@@ -401,6 +408,14 @@ def is_say_screen(screen_name : str):
     def is_save_load_screen(screen_name : str):
         return screen_name in FguiToRenpyConverter.save_load_screen_name_list
 
+    @staticmethod
+    def is_history_screen(screen_name : str):
+        return screen_name in FguiToRenpyConverter.history_screen_name_list
+
+    @staticmethod
+    def is_history_item(screen_name : str):
+        return screen_name in FguiToRenpyConverter.history_item_screen_name_list
+
     def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1):
         screen_ui_code = []
         # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}")
@@ -528,6 +543,14 @@ def generate_screen(self, component : FguiComponent):
             self.generate_save_load_screen(component)
             return
 
+        # history_item和history界面的特殊处理
+        if self.is_history_item(screen_name):
+            self.generate_history_item(component)
+            return
+        if self.is_history_screen(screen_name):
+            self.generate_history_screen(component)
+            return
+
         # confirm 界面固定入参
         if screen_name == 'confirm':
             screen_params = 'message, yes_action, no_action'
@@ -755,6 +778,132 @@ def generate_save_load_screen(self, component : FguiComponent):
         self.screen_code.extend(self.screen_ui_code)
         return
 
+    def generate_history_screen(self, component :FguiComponent):
+        print("This is history screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+        history_item = None
+        displayable_list_len = len(component.display_list.displayable_list)
+        history_list = None
+        history_list_index = -1
+
+        for i in range(displayable_list_len):
+            displayable = component.display_list.displayable_list[i]
+            # 搜索名为 save_slot_list 的列表组件
+            if displayable.name == 'history_list' and isinstance(displayable, FguiList):
+                history_list = displayable
+                history_list_index = i
+                # 确认默认引用组件类型。
+                history_item = self.fgui_assets.get_component_by_id(history_list.default_item_id)
+                break
+        if not history_list:
+            print(f"{component.name} contains no history list.")
+            return
+        # 检查history_list默认引用组件类型是否为component
+        if not isinstance(history_item, FguiComponent):
+            print(f"{component.name} history list item is not Component.")
+            return
+
+        self.reset_indent_level()
+        self.screen_definition_head.append("# 对话历史界面")
+        self.screen_definition_head.append(f"screen {component.name}():")
+        self.indent_level_up()
+        self.screen_definition_head.append(f"{self.indent_str}tag menu")
+        self.screen_definition_head.append(f"{self.indent_str}predict False")
+        
+        # history_list 之前的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=history_list_index))
+
+        # history_list的处理
+        history_list_code = []
+        history_list_code.append(f"{self.indent_str}vpgrid:")
+        self.indent_level_up()
+        # 固定可拖拽
+        history_list_code.append(f"{self.indent_str}draggable True")
+        # 固定一列
+        history_list_code.append(f"{self.indent_str}cols 1")
+        history_list_code.append(f"{self.indent_str}yspacing {history_list.line_gap}")
+        history_list_code.append(f"{self.indent_str}pos {history_list.xypos}")
+        history_list_code.append(f"{self.indent_str}xysize {history_list.size}")
+        history_list_code.append(f"{self.indent_str}for h in _history_list:")
+        self.indent_level_up()
+        # item组件可能包含多个子组件，直接引用会出现vpgrid overfull错误
+        history_list_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        history_list_code.append(f"{self.indent_str}xysize {history_item.size}")
+        history_list_code.append(f"{self.indent_str}use {history_item.name}(h.who, h.what)")
+        self.indent_level_down(3)
+        self.screen_ui_code.extend(history_list_code)
+
+        # history_list 之后的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=history_list_index+1))
+        self.screen_ui_code.append("")
+        self.reset_indent_level()
+
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_ui_code)
+        return
+
+    def generate_history_item(self, component : FguiComponent):
+        print("This is history item.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+        who_text = None
+        what_text = None
+        namebox = None
+        textbox = None
+        namebox_pos = (0, 0)
+        textbox_pos = (0, 0)
+        
+        for displayable in component.display_list.displayable_list:
+            # 生成发言角色名的文本样式
+            if displayable.name == 'who' and isinstance(displayable, FguiText):
+                self.generate_text_style(displayable, "history_who_text_style")
+                who_text = displayable
+            # 生成发言内容的文本样式
+            if displayable.name == 'what' and isinstance(displayable, FguiText):
+                self.generate_text_style(displayable, "history_what_text_style")
+                what_text = displayable
+        # 检查查找结果
+        if who_text == None:
+            print("Lack of who text component.")
+            return
+        if what_text == None:
+            print("Lack of what text component.")
+            return
+
+        who_text_str = f"{who_text.text}".replace("\n", "\\n").replace("\r", "\\n")
+        what_text_str = f"{what_text.text}".replace("\n", "\\n").replace("\r", "\\n")
+        screen_params = f"who='{who_text_str}', what='{what_text_str}'"
+        self.reset_indent_level()
+        # say界面需要覆盖默认gui设置
+        self.screen_definition_head.append("# history_item界面，用于显示一条对话记录。")
+        # self.screen_definition_head.append("style history_label is history_who_text_style")
+        # self.screen_definition_head.append("style history_dialogue is history_what_text_style")
+        self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}text _(who) style 'history_who_text_style':")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {who_text.xypos}")
+        self.indent_level_down()
+        self.screen_ui_code.append(f"{self.indent_str}text _(what) style 'history_what_text_style':")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}pos {what_text.xypos}")
+        self.reset_indent_level()
+
+        self.screen_ui_code.append("")
+        self.screen_code.extend(self.screen_definition_head)
+        self.screen_code.extend(self.screen_ui_code)
+
+
     def generate_say_screen(self, component : FguiComponent):
         print("This is say screen.")
         self.screen_definition_head.clear()
@@ -863,7 +1012,7 @@ def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100,
     def generate_button_parameter(button_title=None, original_actions_str=None, icon=None):
         parameter_str = ""
         title_str = "title=''"
-        actions_str = "actions=None"
+        actions_str = "actions=NullAction()"
         icon_str = "icon=Null()"
         if button_title :
             title_str = f"title=\"{button_title}\"".replace("\n", "\\n").replace("\r", "\\n")
@@ -879,7 +1028,7 @@ def generate_button_parameter(button_title=None, original_actions_str=None, icon
     def generate_button_callable_parameter(button_title=None, actions=None, icon=None):
         parameter_str = ""
         title_str = "title=''"
-        actions_str = "actions=None"
+        actions_str = "actions=NullAction()"
         icon_str = "icon=Null()"
         if button_title :
             title_str = f"title={button_title}".replace("\n", "\\n").replace("\r", "\\n")
@@ -1568,8 +1717,9 @@ def generate_list_displayable(self, fgui_list):
         # 若为组件
         if default_item:
             default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id)
-            default_item_type = 'component'
-        # 若为图片
+            # default_item_type = 'component'
+            default_item_type = default_item.extention
+        # 若非组件
         else:
             default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id)
             if default_item_name:
@@ -1578,43 +1728,110 @@ def generate_list_displayable(self, fgui_list):
                 print("Ref com not found.")
                 return list_code
 
+        # 列表与默认元素尺寸，可能用于grid或vpgrid的行数与列数计算
+        list_xysize = fgui_list.size
+        default_item_size = default_item.size
+        list_length = len(fgui_list.item_list)
+        column_num = 1
+        row_num = 1
+        end_indent_level = 1
+
         # 根据“溢出处理”是否可见区分处理。
         # 若“可见”，则使用hbox、vbox和grid。
         if fgui_list.overflow == "visible":
             # 单列竖排，使用vbox
             if fgui_list.layout == "column":
                 list_code.append(f"{self.indent_str}vbox:")
+                self.indent_level_up()
+                if fgui_list.line_gap:
+                    list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}")
+
             # 单行横排，使用hbox
             elif fgui_list.layout == "row":
                 list_code.append(f"{self.indent_str}hbox:")
+                self.indent_level_up()
+                if fgui_list.line_gap:
+                    list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}")
+
             # 其他，使用grid
             else:
-                list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
+                # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
+                if not (fgui_list.line_item_count and fgui_list.line_item_count2):
+                    # 横向流动，填充行，之后换行
+                    if fgui_list.layout == 'flow_hz' :
+                        column_num = math.floor(list_xysize[0] / default_item_size[0])
+                        row_num = math.ceil(list_length / column_num)
+                    # 纵向流动，先填充列，之后换列
+                    elif fgui_list.layout == 'flow_vt' :
+                        row_num = math.floor(list_xysize[1] / default_item_size[1])
+                        column_num = math.ceil(list_length / row_num)
+                    list_code.append(f"{self.indent_str}grid {column_num} {row_num}:")
+                else:
+                    list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
+                self.indent_level_up()
+                if fgui_list.layout == 'flow_vt' :
+                    list_code.append(f"{self.indent_str}transpose True")
+                if fgui_list.line_gap:
+                    list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
+                if fgui_list.col_gap:
+                    list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
+            list_code.append(f"{self.indent_str}pos {fgui_list.xypos}")
+            list_code.append(f"{self.indent_str}xysize {fgui_list.size}")
 
         else:
-            list_code.append(f"{self.indent_str}vpgrid:")
+            end_indent_level = 2
+            list_code.append(f"{self.indent_str}viewport:")
             self.indent_level_up()
+            list_code.append(f"{self.indent_str}pos {fgui_list.xypos}")
+            list_code.append(f"{self.indent_str}xysize {fgui_list.size}")
+            if fgui_list.margin:
+                list_code.append(f"{self.indent_str}margin {fgui_list.margin}")
 
-            # 若“隐藏”，使用不可滚动的vpgrid
+            # 若“隐藏”，使用不可滚动的viewport
             if fgui_list.overflow == "hidden":
                 list_code.append(f"{self.indent_str}draggable False")
-                # 单列竖排
+                list_code.append(f"{self.indent_str}mousewheel False")
+                # 单列竖排，使用vbox
                 if fgui_list.layout == "column":
-                    cols = 1
-                    rows = item_number
-                # 单行横排
+                    list_code.append(f"{self.indent_str}vbox:")
+                    self.indent_level_up()
+                    if fgui_list.line_gap:
+                        list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}")
+
+                # 单行横排，使用hbox
                 elif fgui_list.layout == "row":
-                    cols = item_number
-                    rows = 1
-                # 其他
+                    list_code.append(f"{self.indent_str}hbox:")
+                    self.indent_level_up()
+                    if fgui_list.line_gap:
+                        list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}")
+
+                # 其他，使用grid
                 else:
-                    cols = fgui_list.line_item_count
-                    rows = fgui_list.line_item_count2
-                list_code.append(f"{self.indent_str}cols {cols}")
-                list_code.append(f"{self.indent_str}rows {rows}")
+                    # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
+                    if not (fgui_list.line_item_count and fgui_list.line_item_count2):
+                        # 横向流动，填充行，之后换行
+                        if fgui_list.layout == 'flow_hz' :
+                            column_num = math.floor(list_xysize[0] / default_item_size[0])
+                            row_num = math.ceil(list_length / column_num)
+                        # 纵向流动，先填充列，之后换列
+                        elif fgui_list.layout == 'flow_vt' :
+                            row_num = math.floor(list_xysize[1] / default_item_size[1])
+                            column_num = math.ceil(list_length / row_num)
+                        list_code.append(f"{self.indent_str}grid {column_num} {row_num}:")
+                    else:
+                        list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
+                    self.indent_level_up()
+                    if fgui_list.layout == 'flow_vt' :
+                        list_code.append(f"{self.indent_str}transpose True")
+                    if fgui_list.line_gap:
+                        list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
+                    if fgui_list.col_gap:
+                        list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
+
             # 若“滚动”，使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。
             elif fgui_list.overflow == "scroll":
                 list_code.append(f"{self.indent_str}draggable True")
+                list_code.append(f"{self.indent_str}mousewheel True")
                 # 垂直滚动
                 if fgui_list.scroll == "vertical":
                     pass
@@ -1626,43 +1843,63 @@ def generate_list_displayable(self, fgui_list):
                     pass
                 # 单列竖排，使用vbox
                 if fgui_list.layout == "column":
-                    cols = 1
-                    rows = item_number
+                    list_code.append(f"{self.indent_str}vbox:")
+                    self.indent_level_up()
+                    if fgui_list.line_gap:
+                        list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}")
+
                 # 单行横排，使用hbox
                 elif fgui_list.layout == "row":
-                    cols = item_number
-                    rows = 1
+                    list_code.append(f"{self.indent_str}hbox:")
+                    self.indent_level_up()
+                    if fgui_list.line_gap:
+                        list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}")
+
                 # 其他，使用grid
                 else:
-                    cols = fgui_list.line_item_count
-                    rows = fgui_list.line_item_count2
-                list_code.append(f"{self.indent_str}cols {cols}")
-                list_code.append(f"{self.indent_str}rows {rows}")
-                if fgui_list.line_gap:
-                    list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
-                if fgui_list.col_gap:
-                    list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
-            self.indent_level_down()
+                    # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
+                    if not (fgui_list.line_item_count and fgui_list.line_item_count2):
+                        # 横向流动，填充行，之后换行
+                        if fgui_list.layout == 'flow_hz' :
+                            column_num = math.floor(list_xysize[0] / default_item_size[0])
+                            row_num = math.ceil(list_length / column_num)
+                        # 纵向流动，先填充列，之后换列
+                        elif fgui_list.layout == 'flow_vt' :
+                            row_num = math.floor(list_xysize[1] / default_item_size[1])
+                            column_num = math.ceil(list_length / row_num)
+                        list_code.append(f"{self.indent_str}grid {column_num} {row_num}:")
+                    else:
+                        list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
+                    self.indent_level_up()
+                    if fgui_list.layout == 'flow_vt' :
+                        list_code.append(f"{self.indent_str}transpose True")
+                    if fgui_list.line_gap:
+                        list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
+                    if fgui_list.col_gap:
+                        list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
 
-        self.indent_level_up()
-        list_code.append(f"{self.indent_str}pos {fgui_list.xypos}")
-        list_code.append(f"{self.indent_str}xysize {fgui_list.size}")
-        if fgui_list.margin:
-            list_code.append(f"{self.indent_str}margin {fgui_list.margin}")
         # 添加元素
         for item in fgui_list.item_list:
             # 非默认元素
             if item.item_url:
+                # TODO 非默认元素待处理
                 pass
             # 默认元素
             else:
                 if default_item_type == "image":
                     list_code.append(f"{self.indent_str}add \'{default_item_name}\'")
-                elif default_item_type == "component":
+                elif default_item_type == "Button":
                     parameter_str = self.generate_button_parameter(item.item_title)
                     list_code.append(f"{self.indent_str}use {default_item_name}({parameter_str})")
+                else:
+                    # 非按钮组件可能包含多个子组件，直接引用会出现vpgrid overfull错误
+                    list_code.append(f"{self.indent_str}fixed:")
+                    self.indent_level_up()
+                    list_code.append(f"{self.indent_str}xysize {default_item.size}")
+                    list_code.append(f"{self.indent_str}use {default_item_name}()")
+                    self.indent_level_down()
 
-        self.indent_level_down()
+        self.indent_level_down(end_indent_level)
         return list_code
 
     def generate_renpy_code(self):

From 96f9289ec154c9d2884191d0b29a58db9e9a7d7a Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 14 Nov 2025 00:41:47 +0800
Subject: [PATCH 15/40] Children of screen could be shown or hide by Fgui
 controllers.

---
 src/fgui_converter/FguiAssetsParseLib.py      | 11 +++-
 .../utils/renpy/Fgui2RenpyConverter.py        | 59 ++++++++++++++++---
 2 files changed, 60 insertions(+), 10 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 9a974a4..506f797 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -580,7 +580,8 @@ def __init__(self, component_property_tree):
 class FguiButtonProperty(FguiComponentPropertyBase):
     """
     组件子属性中的Button信息。
-    通常包括title和icon，分别表示标题(文本)和图标(装载器)
+    包括title和icon，分别表示标题(文本)和图标(装载器)。
+    若按钮与控制器连接，在此处记录关联控制器名与关联索引。
     """
     def __init__(self, component_property_tree):
         super().__init__(component_property_tree)
@@ -590,7 +591,10 @@ def __init__(self, component_property_tree):
         self.selected_title = self.component_property_tree.get("selectedTitle")
         self.icon = self.component_property_tree.get("icon")
         self.selected_icon = self.component_property_tree.get("selectedIcon")
-
+        self.checked = self.component_property_tree.get("checked", False)
+        self.controller_name = self.component_property_tree.get("controller")
+        self.controller_index = self.component_property_tree.get("page")
+        # TODO 待添加点击音效
 
 class FguiGraph(FguiDisplayable):
     """
@@ -841,7 +845,8 @@ def __init__(self, gear_item_tree):
         controller_index = gear_item_tree.get("pages")
         values = gear_item_tree.get("values")
         if controller_index:
-            self.controller_index = controller_index.split(",")
+            self.controller_index = [int(i) for i in controller_index.split(",")]
+            print(self.controller_index)
         self.values = None
         if values:
             self.values = values.split("|")
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index bb28c6a..5174ed0 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -439,6 +439,15 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                 pass
             # 其他组件
             else:
+                end_indent_level = 1
+
+                # 根据显示控制器gearDisplay设置显示条件
+                if displayable.gear_display:
+                    condition_str = f"showif {displayable.gear_display.controller_name} in {displayable.gear_display.controller_index}:"
+                    screen_ui_code.append(f"{self.indent_str}{condition_str}")
+                    self.indent_level_up()
+                    end_indent_level = 2
+                
                 # 根据引用源id查找组件
                 ref_com = self.fgui_assets.get_component_by_id(displayable.src)
                 # 按钮。可设置标题，并根据自定义数据字段设置action。
@@ -448,13 +457,20 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。
                     actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data
+                    action_list = []
+                    if actions:
+                        action_list.append(actions)
                     # 此处仅处理了title，而未处理selected_title。后续可能需要添加。
                     if displayable.button_property:
+                        if displayable.button_property.controller_name:
+                            button_controller_action = f"SetScreenVariable('{displayable.button_property.controller_name}', {displayable.button_property.controller_index})"
+                            action_list.append(button_controller_action)
+                            actions = f"[{', '.join(action_list)}]"
                         parameter_str = self.generate_button_parameter(displayable.button_property.title, actions)
                     else:
                         parameter_str = self.generate_button_parameter(None, actions)
                     screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{component.name}_{displayable.id}'")
-                    self.indent_level_down()
+                    self.indent_level_down(end_indent_level)
                     continue
                 # 滑动条
                 if ref_com.extention == "Slider" and ref_com.name != None:
@@ -473,16 +489,15 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                         screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.slider_property.current_value)}")
                         bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.slider_property.min_value, max_value=displayable.slider_property.max_value)
                     screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'")
-                    self.indent_level_down()
+                    self.indent_level_down(end_indent_level)
                     continue
                 # 其他组件
                 screen_ui_code.append(f"{self.indent_str}fixed:")
                 self.indent_level_up()
                 screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                 screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'")
-                self.indent_level_down()
-        # print("convert_component_display_list method:")
-        # print(screen_ui_code)
+                self.indent_level_down(end_indent_level)
+
         return screen_ui_code
 
     def generate_screen(self, component : FguiComponent):
@@ -566,6 +581,16 @@ def generate_screen(self, component : FguiComponent):
         if self.is_modal_screen(screen_name):
             self.screen_ui_code.append(f"{self.indent_str}modal True")
             self.screen_ui_code.append(f"{self.indent_str}zorder 200\n")
+
+        # 根据控制器列表定义界面内变量
+        if component.controller_list:
+            self.screen_variable_code.append(f"{self.indent_str}# 由组件控制器生成的界面内控制变量：") 
+        for controller in component.controller_list:
+            if not isinstance(controller, FguiController):
+                print("Component controller object type is wrong.")
+                break
+            self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}")            
+
         self.screen_ui_code.extend(self.convert_component_display_list(component))
 
         self.screen_code.extend(self.screen_definition_head)
@@ -1283,7 +1308,7 @@ def generate_button_screen(self, component):
             # 其他状态根据枚举值加入各列表
             else:
                 for i in range(0, state_number):
-                    if str(i) in displayable.gear_display.controller_index:
+                    if i in displayable.gear_display.controller_index:
                         state_children_dict[state_index_name_dict[i]].append((displayable, FguiToRenpyConverter.get_child_type(displayable)))
                         continue
             # 图片组件
@@ -1488,6 +1513,15 @@ def generate_image_displayable(self, fgui_image : FguiImage):
             print("It is not a image displayable.")
             return image_code
 
+        end_indent_level = 0
+
+        # 根据显示控制器gearDisplay设置显示条件
+        if fgui_image.gear_display:
+            condition_str = f"showif {fgui_image.gear_display.controller_name} in {fgui_image.gear_display.controller_index}:"
+            image_code.append(f"{self.indent_str}{condition_str}")
+            self.indent_level_up()
+            end_indent_level = 1
+
         for image in self.fgui_assets.package_desc.image_list:
             if fgui_image.src == image.id:
                 image_name = image.name
@@ -1525,6 +1559,7 @@ def generate_image_displayable(self, fgui_image : FguiImage):
                     image_code.append(f"{self.indent_str}xysize {size}")
                 self.indent_level_down()
                 break
+        self.indent_level_down(end_indent_level)
         return image_code
 
     def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : str) -> list:
@@ -1559,6 +1594,16 @@ def generate_text_displayable(self, fgui_text):
             print("It is not a text displayable.")
             return text_code
 
+
+        end_indent_level = 1
+
+        # 根据显示控制器gearDisplay设置显示条件
+        if fgui_text.gear_display:
+            condition_str = f"showif {fgui_text.gear_display.controller_name} in {fgui_text.gear_display.controller_index}:"
+            text_code.append(f"{self.indent_str}{condition_str}")
+            self.indent_level_up()
+            end_indent_level = 2
+
         # 直接定义text组件。
         # 处理换行符
         text_str = fgui_text.text.replace("\n", "\\n").replace("\r", "\\n")
@@ -1697,7 +1742,7 @@ def generate_text_displayable(self, fgui_text):
         if fgui_text.is_input:
             self.indent_level_down()
 
-        self.indent_level_down()
+        self.indent_level_down(end_indent_level)
         return text_code
 
     def generate_list_displayable(self, fgui_list):

From 1b3e15f9a813e4a12d8afc95008e63ec3437f071 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 14 Nov 2025 11:09:11 +0800
Subject: [PATCH 16/40] Fix issue of save/load screen lack of tag menu.

---
 src/fgui_converter/FguiAssetsParseLib.py              | 1 -
 src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py | 3 +++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 506f797..de0e7d5 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -846,7 +846,6 @@ def __init__(self, gear_item_tree):
         values = gear_item_tree.get("values")
         if controller_index:
             self.controller_index = [int(i) for i in controller_index.split(",")]
-            print(self.controller_index)
         self.values = None
         if values:
             self.values = values.split("|")
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 5174ed0..142f4f9 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -708,6 +708,9 @@ def generate_save_load_screen(self, component : FguiComponent):
         
         self.screen_definition_head.append("# 存档/读档 界面")
         self.screen_definition_head.append(f"screen {component.name}():")
+
+        # 菜单标签
+        self.screen_definition_head.append(f"{self.indent_str}tag menu")
         # save_slot_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=slot_list_index))
 

From dc54c125cb717cf0d6d590675a6330863f715fd3 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 8 Dec 2025 18:19:49 +0800
Subject: [PATCH 17/40] Implementation hidden(part display) and scrollable with
 screen level.

---
 src/fgui_converter/FguiAssetsParseLib.py      |  9 ++++
 .../utils/renpy/Fgui2RenpyConverter.py        | 46 +++++++++++++++++--
 2 files changed, 52 insertions(+), 3 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index de0e7d5..b2353ab 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -261,6 +261,8 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
         self.package_desc_id = package_desc_id
         size = component_etree.get("size")
         self.size = tuple(map(int, size.split(","))) if size else (0,0)
+        self.overflow = component_etree.get("overflow", "visible")
+        self.scroll = component_etree.get("scroll", "vertical")
         self.extention = component_etree.get("extention")
         self.mask = component_etree.get("mask")
         # 轴心。默认值为(0.0, 0.0)。
@@ -272,6 +274,10 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
         # 自定义数据。实际使用时，相同id的FguiDisplayable中的自定义数据优先。
         self.custom_data = component_etree.get("customData")
 
+        #包含所有子组件的包围框尺寸，初始值为组件尺寸。
+        self.bbox_width = self.size[0]
+        self.bbox_height = self.size[1]
+
         # 控制器，一般不超过1个。
         self.controller_list = []
         # 显示内容列表，通常为image、text、graph。
@@ -286,6 +292,9 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
             # 显示内容
             elif (self.component_etree[i].tag == "displayList"):
                 self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id)
+                for displayable in self.display_list.displayable_list:
+                    self.bbox_width = max(self.bbox_width, displayable.xypos[0] + displayable.size[0])
+                    self.bbox_height = max(self.bbox_height, displayable.xypos[1] + displayable.size[1])
 
     def __repr__(self):
         return f"FguiComponent({self.id}, {self.name}, {self.size}, {self.extention}, {self.mask})"
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 142f4f9..ab4514c 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -591,6 +591,22 @@ def generate_screen(self, component : FguiComponent):
                 break
             self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}")            
 
+        # 根据组件的可见区域性质，决定是否加一层viewport。
+        if component.overflow == "hidden":
+            self.screen_ui_code.append(f"{self.indent_str}viewport:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}")
+        elif component.overflow == "scroll":
+            self.screen_ui_code.append(f"{self.indent_str}viewport:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}")
+            self.screen_ui_code.append(f"{self.indent_str}draggable True") 
+            # 在Ren'Py中实际可能无法滚动。
+            # 需要添加一个fixed组件，并设置一个合适的xysize。该xysize应为容纳所有子组件的包围框。
+            self.screen_ui_code.append(f"{self.indent_str}fixed:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize ({component.bbox_width}, {component.bbox_height})")
+
         self.screen_ui_code.extend(self.convert_component_display_list(component))
 
         self.screen_code.extend(self.screen_definition_head)
@@ -890,16 +906,23 @@ def generate_history_item(self, component : FguiComponent):
         textbox = None
         namebox_pos = (0, 0)
         textbox_pos = (0, 0)
+        who_index = 0
+        what_index = 0
         
-        for displayable in component.display_list.displayable_list:
+        display_list_len = len(component.display_list.displayable_list)
+        for i in range(display_list_len):
+            displayable = component.display_list.displayable_list[i]
             # 生成发言角色名的文本样式
             if displayable.name == 'who' and isinstance(displayable, FguiText):
                 self.generate_text_style(displayable, "history_who_text_style")
                 who_text = displayable
+                who_index = i
             # 生成发言内容的文本样式
             if displayable.name == 'what' and isinstance(displayable, FguiText):
                 self.generate_text_style(displayable, "history_what_text_style")
                 what_text = displayable
+                what_index = i
+
         # 检查查找结果
         if who_text == None:
             print("Lack of who text component.")
@@ -908,8 +931,10 @@ def generate_history_item(self, component : FguiComponent):
             print("Lack of what text component.")
             return
 
-        who_text_str = f"{who_text.text}".replace("\n", "\\n").replace("\r", "\\n")
-        what_text_str = f"{what_text.text}".replace("\n", "\\n").replace("\r", "\\n")
+        # who_text_str = f"{who_text.text}".replace("\n", "\\n").replace("\r", "\\n")
+        # what_text_str = f"{what_text.text}".replace("\n", "\\n").replace("\r", "\\n")
+        who_text_str = ''
+        what_text_str = '无对话记录。'
         screen_params = f"who='{who_text_str}', what='{what_text_str}'"
         self.reset_indent_level()
         # say界面需要覆盖默认gui设置
@@ -918,6 +943,15 @@ def generate_history_item(self, component : FguiComponent):
         # self.screen_definition_head.append("style history_dialogue is history_what_text_style")
         self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
         self.indent_level_up()
+
+        # 不包含who和what的displayList部分按顺序放在前面
+        index_min = min(what_index, who_index)
+        index_max = max(what_index, who_index)
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=index_min))
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=index_min+1, list_end_index=index_max))
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=index_max+1))
+
+        # who和what固定放在后面，显示在其他组件之上。
         self.screen_ui_code.append(f"{self.indent_str}text _(who) style 'history_who_text_style':")
         self.indent_level_up()
         self.screen_ui_code.append(f"{self.indent_str}pos {who_text.xypos}")
@@ -953,6 +987,7 @@ def generate_say_screen(self, component : FguiComponent):
             # 生成发言角色名的文本样式
             if displayable.name == 'who' and isinstance(displayable, FguiText):
                 self.generate_text_style(displayable, "say_who_text_style")
+                self.generate_text_style(displayable, "say_label")
                 who_text = displayable
             # 生成发言内容的文本样式
             if displayable.name == 'what' and isinstance(displayable, FguiText):
@@ -1092,6 +1127,7 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str):
         self.style_code.append(f"# 文本{fgui_text.name}样式定义")
         # 定义样式
         self.style_code.append(f"style {style_name}:")
+        self.style_code.append(f"{style_indent}anchor {fgui_text.pivot}")
         self.style_code.append(f"{style_indent}xysize {fgui_text.size}")
         # 字体可能为空，改为Ren'Py内置默认字体SourceHanSansLite
         text_font =  fgui_text.font if fgui_text.font else "SourceHanSansLite"
@@ -1721,6 +1757,10 @@ def generate_text_displayable(self, fgui_text):
         # Ren'Py中只有文本宽度小于组件宽度的水平方向对齐设置
         xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
         text_code.append(f"{self.indent_str}textalign {xalign}")
+        # 不自动换行
+        text_code.append(f"{self.indent_str}layout 'nobreak'")
+            
+
         # 粗体、斜体、下划线、删除线
         if fgui_text.bold:
             text_code.append(f"{self.indent_str}bold {fgui_text.bold}")

From a24f709e0882a53a3ae7d8676bad7676789a3902 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 6 Jan 2026 18:31:37 +0800
Subject: [PATCH 18/40] Implementation of screen "gallery".

1. Add "gallery" template.
2. "gallery" component is marked special screen, with specific converting method.
---
 src/fgui_converter/FguiAssetsParseLib.py      |   1 +
 .../utils/renpy/Fgui2RenpyConverter.py        | 208 +++++++++++++++++-
 .../renpy_gallery_template.txt                |  30 +++
 3 files changed, 230 insertions(+), 9 deletions(-)
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index b2353ab..c03224f 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -293,6 +293,7 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
             elif (self.component_etree[i].tag == "displayList"):
                 self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id)
                 for displayable in self.display_list.displayable_list:
+                    # 包围框尺寸。FGUI不考虑子组件坐标xy小于0的情况，仅扩展组件的右侧和下方。
                     self.bbox_width = max(self.bbox_width, displayable.xypos[0] + displayable.size[0])
                     self.bbox_height = max(self.bbox_height, displayable.xypos[1] + displayable.size[1])
 
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index ab4514c..19a5cf0 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -45,7 +45,7 @@ class FguiToRenpyConverter:
     default_background = '#fff'
 
     # 菜单类界面名称，需要添加 tag menu。
-    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about', 'gallery']
+    menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about']
 
     # 模态类界面名称，需要添加 Modal True。
     modal_screen_name_list = ['confirm']
@@ -63,6 +63,12 @@ class FguiToRenpyConverter:
     history_screen_name_list = ['history']
     history_item_screen_name_list = ['history_item']
     
+    # 图鉴类界面
+    gallery_screen_name_list = ['gallery', 'music_room']
+
+    # 所以特殊界面名
+    special_screen_name_list = []
+
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
         self.renpy_code = []
@@ -75,6 +81,11 @@ def __init__(self, fgui_assets):
         self.image_definition_code = []
         self.graph_definition_code = []
         self.game_global_variables_code = []
+        self.gallery_screen_code = []
+        self.music_room_screen_code = []
+
+        # 所有特殊界面名
+        self.special_screen_name_list = self.menu_screen_name_list + self.modal_screen_name_list + self.choice_screen_name_list + self.say_screen_name_list + self.save_load_screen_name_list + self.history_screen_name_list + self.gallery_screen_name_list
         
         # dismiss用于取消一些组件的focus状态，例如input。
         self.screen_has_dismiss = False
@@ -94,9 +105,15 @@ def __init__(self, fgui_assets):
                              os.path.join(os.path.dirname(os.path.abspath(__file__)), "renpy_templates"))
         self.font_map_template = 'renpy_font_map_definition.txt'
         self.graph_template_dict = {}
-        self.graph_template_dict['null'] = self.get_graph_template('renpy_null_template.txt')
-        self.graph_template_dict['rectangle'] = self.get_graph_template('renpy_rectangle_template.txt')
-        self.graph_template_dict['ellipse'] = self.get_graph_template('renpy_ellipse_template.txt')
+        self.graph_template_dict['null'] = self.get_template_content('renpy_null_template.txt')
+        self.graph_template_dict['rectangle'] = self.get_template_content('renpy_rectangle_template.txt')
+        self.graph_template_dict['ellipse'] = self.get_template_content('renpy_ellipse_template.txt')
+        self.gallery_template_dict = {}
+        self.gallery_template_dict['gallery'] = self.get_template_content('renpy_gallery_template.txt')
+        self.gallery_template_dict['music_room'] = self.get_template_content('renpy_music_room_template.txt')
+
+        # 输出游戏目录
+        self.game_dir = None
 
     def set_game_global_variables(self, variable_name, variable_value):
         variable_str = f"define {variable_name} = {variable_value}"
@@ -416,6 +433,14 @@ def is_history_screen(screen_name : str):
     def is_history_item(screen_name : str):
         return screen_name in FguiToRenpyConverter.history_item_screen_name_list
 
+    @staticmethod
+    def is_gallery_screen(screen_name : str):
+        return screen_name == 'gallery'
+
+    @staticmethod
+    def is_music_room_screen(screen_name : str):
+        return screen_name == 'music_room'
+
     def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1):
         screen_ui_code = []
         # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}")
@@ -543,6 +568,7 @@ def generate_screen(self, component : FguiComponent):
 
         # 界面入参列表
         screen_params = ''
+
         # choice界面的特殊处理
         if self.is_choice_screen(screen_name):
             self.generate_choice_screen(component)
@@ -566,6 +592,14 @@ def generate_screen(self, component : FguiComponent):
             self.generate_history_screen(component)
             return
 
+        # gallery和music_room界面的特殊处理
+        if self.is_gallery_screen(screen_name):
+            self.generate_gallery_screen(component)
+            return
+        if self.is_music_room_screen(screen_name):
+            self.generate_music_room_screen(component)
+            return
+
         # confirm 界面固定入参
         if screen_name == 'confirm':
             screen_params = 'message, yes_action, no_action'
@@ -965,6 +999,137 @@ def generate_history_item(self, component : FguiComponent):
         self.screen_code.extend(self.screen_definition_head)
         self.screen_code.extend(self.screen_ui_code)
 
+    def generate_gallery_screen(self, component : FguiComponent):
+        print("This is gallery screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+        self.gallery_screen_code.clear()
+
+        if not self.gallery_template_dict['gallery']:
+            print("Gallery template is empty.")
+            return
+        
+        gallery_button_list = None
+        gallery_button_list_item = None
+        gallery_button_list_index = -1
+        displayable_list_len = len(component.display_list.displayable_list)
+        gallery_button_list_len = 1
+        gallery_button_list_column =  1
+        gallery_button_list_row = 1
+        ui_helper_exists = False
+        # 从game目录中 01_ui_helper.rpy 文件中获取实际的 gallery_image_list。
+        # 此处只检查文件是否存在，不检查文件内容。
+        gallery_image_list_file = os.path.join(self.game_dir, '01_ui_helper.rpy')
+        if not os.path.exists(gallery_image_list_file):
+            print(f"Gallery image list file {gallery_image_list_file} does not exist.")
+        else:
+            ui_helper_exists = True
+
+        for i in range(displayable_list_len):
+            displayable = component.display_list.displayable_list[i]
+            # 搜索名为 gallery_button_list 的列表组件
+            if displayable.name == 'gallery_button_list' and isinstance(displayable, FguiList):
+                gallery_button_list = displayable
+                # 确认默认引用组件类型。
+                gallery_button_list_item = self.fgui_assets.get_component_by_id(gallery_button_list.default_item_id)
+                gallery_button_list_index = i
+                break
+        if not gallery_button_list:
+            print(f"{component.name} contains no gallery button list.")
+            return
+        # 检查gallery_button_list_item是否为按钮
+        if not isinstance(gallery_button_list_item, FguiButton):
+            print(f"{component.name} gallery button list item is not Button.")
+            return
+
+        # 计算列表每行显示的按钮数量
+        gallery_button_list_len = max(len(displayable.item_list), 1)
+        gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0])
+        gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column)
+
+        self.reset_indent_level()
+        self.screen_definition_head.append("# CG图鉴界面")
+        self.screen_definition_head.append(f"screen {component.name}():")
+        self.indent_level_up()
+        self.screen_definition_head.append(f"{self.indent_str}tag menu")
+        self.screen_definition_head.append(f"{self.indent_str}")
+
+        # gallery_button_list 之前的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=gallery_button_list_index))
+
+        # gallery_button_list 的处理
+        gallery_button_list_code = []
+        gallery_button_list_code.append(f"{self.indent_str}viewport:")
+        self.indent_level_up()
+        gallery_button_list_code.append(f"{self.indent_str}pos {gallery_button_list.xypos}")
+        gallery_button_list_code.append(f"{self.indent_str}xysize {gallery_button_list.size}")
+        # 固定可拖拽
+        gallery_button_list_code.append(f"{self.indent_str}draggable True")
+        gallery_button_list_code.append(f"{self.indent_str}mousewheel True")
+        gallery_button_list_code.append(f"{self.indent_str}grid gallery_view_column gallery_view_row:")
+        self.indent_level_up()
+        gallery_button_list_code.append(f"{self.indent_str}yspacing {gallery_button_list.line_gap}")
+        gallery_button_list_code.append(f"{self.indent_str}xspacing {gallery_button_list.col_gap}")
+        # 若ui_helper.rpy不存在，则根据列表长度添加对应数量的默认按钮
+        if not ui_helper_exists:
+            for i in range(len(gallery_button_list.item_list)):
+                gallery_button_list_code.append(f"{self.indent_str}use {gallery_button_list_item.name}()")
+        # 若ui_helper.rpy存在，则使用固定脚本定义的按钮
+        else:
+            gallery_button_list_code.append(f"{self.indent_str}for i in range(gallery_button_num):")
+            self.indent_level_up()
+            gallery_button_list_code.append(f"{self.indent_str}python:")
+            self.indent_level_up()
+            gallery_button_list_code.append(f"{self.indent_str}gallery_image = gallery_image_list[i]")
+            gallery_button_list_code.append(f"{self.indent_str}if isinstance(gallery_image, list):")            
+            self.indent_level_up()
+            gallery_button_list_code.append(f"{self.indent_str}gallery_image = gallery_image[0]")
+            self.indent_level_down(2)
+            # 暂时用按钮的第一个display作为locked状态的显示内容。
+            locked_image = self.fgui_assets.get_componentname_by_id(gallery_button_list_item.display_list.displayable_list[0].src)
+            gallery_button_list_code.append(f"{self.indent_str}add g.make_button(gallery_image, gallery_image, locked='{locked_image}') xysize {gallery_button_list_item.size}")
+            self.indent_level_down()
+        self.screen_ui_code.extend(gallery_button_list_code)
+
+        self.indent_level_down(2)
+        # gallery_button_list 之后的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=gallery_button_list_index+1))
+        self.screen_ui_code.append("")
+        self.reset_indent_level()
+
+        self.gallery_screen_code.extend(self.screen_definition_head)
+        self.gallery_screen_code.extend(self.screen_ui_code)
+        # 读取 templates/renpy_gallery_template.txt 模板内容，并替换 {gallery_screen_code} 为 self.gallery_screen_code
+        gallery_template_content = self.get_template_content('renpy_gallery_template.txt')
+        gallery_template_content = gallery_template_content.replace('{gallery_screen_code}', '\n'.join(self.gallery_screen_code))
+        gallery_template_content = gallery_template_content.replace('{gallery_button_list_len}', str(gallery_button_list_len))
+        gallery_template_content = gallery_template_content.replace('{gallery_button_list_column}', str(gallery_button_list_column))
+        gallery_template_content = gallery_template_content.replace('{gallery_button_list_row}', str(gallery_button_list_row))
+        self.gallery_screen_code = gallery_template_content.split('\n')
+
+
+        # 将内容保存在 game/scripts/gallery_screen.rpy 文件中
+        gallery_screen_file = os.path.join(self.game_dir, 'scripts', 'gallery_screen.rpy')
+        self.save_code_to_file(gallery_screen_file, self.gallery_screen_code)
+        print(f"Gallery screen code has been saved to {gallery_screen_file}.")
+        return
+
+    def generate_music_room_screen(self, component : FguiComponent):
+        print("This is music room screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+
+        if not self.gallery_template_dict['music_room']:
+            print("Music room template is empty.")
+            return
 
     def generate_say_screen(self, component : FguiComponent):
         print("This is say screen.")
@@ -2037,6 +2202,16 @@ def save_to_file(self, filename):
 
         print(f"Ren'Py代码已保存到: {filename}")
 
+    def save_code_to_file(self, filename : str, code : list[str]) -> None:
+        """
+        保存Ren'Py代码
+        """
+        with open(filename, 'w', encoding='utf-8') as f:
+            for line in code:
+                f.write(line + '\n')
+
+        print(f"Ren'Py代码已保存到: {filename}")
+
     def from_templates_to_renpy(self, filename):
         """
         读取模板替换字符串并保存至Ren'Py目录
@@ -2050,11 +2225,13 @@ def from_templates_to_renpy(self, filename):
         with open(filename, 'w', encoding='utf-8') as f:
             f.write(content)
 
-    def get_graph_template(self, filename):
+    def get_template_content(self, filename : str, path : str = None) -> str:
         """
-        获取graph对应的image对象定义模板
+        获取模板内容
         """
-        with open(os.path.join(self.renpy_template_dir, filename), 'r', encoding='utf-8') as file:
+        template_path = self.renpy_template_dir if path is None else path
+
+        with open(os.path.join(template_path, filename), 'r', encoding='utf-8') as file:
             content = file.read()
         return content
 
@@ -2200,6 +2377,7 @@ def convert(argv):
         # 创建转换器
         print("正在创建转换器...")
         converter = FguiToRenpyConverter(fgui_assets)
+        converter.game_dir = game_dir
         print("转换器创建完成")
 
         # 生成Ren'Py代码
@@ -2208,8 +2386,20 @@ def convert(argv):
         print("Ren'Py代码生成完成")
 
         # 保存.rpy文件到game目录
-        output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy")
-        converter.save_to_file(output_file)
+        # output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy")
+        global_variables_output_file = os.path.join(scripts_dir, "preppipe_global_variables.rpy")
+        image_definition_output_file = os.path.join(scripts_dir, "preppipe_image_definition.rpy")
+        style_output_file = os.path.join(scripts_dir, "preppipe_styles.rpy")
+        screen_output_file = os.path.join(scripts_dir, "preppipe_screens.rpy")
+        # converter.save_to_file(output_file)
+        # self.renpy_code.extend(self.game_global_variables_code)
+        # self.renpy_code.extend(self.image_definition_code)
+        # self.renpy_code.extend(self.graph_definition_code)
+        # self.renpy_code.extend(self.style_code)
+        converter.save_code_to_file(global_variables_output_file, converter.game_global_variables_code)
+        converter.save_code_to_file(image_definition_output_file, converter.image_definition_code)
+        converter.save_code_to_file(style_output_file, converter.style_code)
+        converter.save_code_to_file(screen_output_file, converter.screen_code)
 
         # 部分预定义模板文件修改参数并保存
         font_map_definition_file = os.path.join(scripts_dir, "font_map.rpy")
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt
new file mode 100644
index 0000000..f19f179
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt
@@ -0,0 +1,30 @@
+transform gallery_full_screen:
+    xpos 0
+    ypos 0
+    xsize 1.0
+    ysize 1.0
+
+init python:
+    g = Gallery()
+    for e in gallery_image_list:
+        if isinstance(e, list):
+            # CG组有多个图片
+            # 使用第一个图片的名称
+            g.button(e[0])
+            for img in e:
+                g.image(img)
+                g.unlock(img)
+                g.transform(gallery_full_screen)
+        else:
+            # 单个图片
+            g.button(e)
+            g.image(e)
+            g.unlock(e)
+            g.transform(gallery_full_screen)
+    # gallery_button_num = len(gallery_image_list)
+    gallery_button_num = {gallery_button_list_len}
+    g.transition = None
+    gallery_view_column = {gallery_button_list_column}
+    gallery_view_row = {gallery_button_list_row}
+
+{gallery_screen_code}
\ No newline at end of file

From dfe49e269d54b21197e8826c18c733067a534047 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sun, 11 Jan 2026 01:08:26 +0800
Subject: [PATCH 19/40] Add music_room screen implementation

Add music_room screen.
---
 .../utils/renpy/Fgui2RenpyConverter.py        | 231 ++++++++++++++++--
 .../renpy_gallery_template.txt                |   1 -
 .../renpy_music_room_template.txt             |  29 +++
 3 files changed, 244 insertions(+), 17 deletions(-)
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 19a5cf0..10f0e6a 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -8,6 +8,7 @@
 import argparse
 import math
 import shutil
+import ast
 from enum import IntEnum
 
 # 复合型组件的子组件枚举类型
@@ -69,6 +70,9 @@ class FguiToRenpyConverter:
     # 所以特殊界面名
     special_screen_name_list = []
 
+    # 主管线提供的gallery数据文件
+    ui_helper_file_name = '01_ui_helper.rpy'
+
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
         self.renpy_code = []
@@ -115,6 +119,9 @@ def __init__(self, fgui_assets):
         # 输出游戏目录
         self.game_dir = None
 
+        # gallery相关数据
+        self.gallery_data = {}
+
     def set_game_global_variables(self, variable_name, variable_value):
         variable_str = f"define {variable_name} = {variable_value}"
         self.game_global_variables_code.append(variable_str)
@@ -441,7 +448,7 @@ def is_gallery_screen(screen_name : str):
     def is_music_room_screen(screen_name : str):
         return screen_name == 'music_room'
 
-    def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1):
+    def convert_component_display_list(self, component: FguiComponent, list_begin_index=0, list_end_index=-1) -> list:
         screen_ui_code = []
         # print(f"list_begin_index: {list_begin_index}, list_end_index: {list_end_index}")
         end_index = len(component.display_list.displayable_list) if (list_end_index == -1) else list_end_index
@@ -1021,13 +1028,11 @@ def generate_gallery_screen(self, component : FguiComponent):
         gallery_button_list_column =  1
         gallery_button_list_row = 1
         ui_helper_exists = False
-        # 从game目录中 01_ui_helper.rpy 文件中获取实际的 gallery_image_list。
-        # 此处只检查文件是否存在，不检查文件内容。
-        gallery_image_list_file = os.path.join(self.game_dir, '01_ui_helper.rpy')
-        if not os.path.exists(gallery_image_list_file):
-            print(f"Gallery image list file {gallery_image_list_file} does not exist.")
-        else:
+        # 检查 gallery_data 是否存在gallery_image_list
+        if 'gallery_image_list' in self.gallery_data:
             ui_helper_exists = True
+        else:
+            print(f"Gallery image list is not defined in {self.ui_helper_file_name}.")
 
         for i in range(displayable_list_len):
             displayable = component.display_list.displayable_list[i]
@@ -1046,10 +1051,24 @@ def generate_gallery_screen(self, component : FguiComponent):
             print(f"{component.name} gallery button list item is not Button.")
             return
 
-        # 计算列表每行显示的按钮数量
-        gallery_button_list_len = max(len(displayable.item_list), 1)
-        gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0])
-        gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column)
+        # 计算列表的行数与列数
+        if ui_helper_exists:
+            gallery_button_list_len = len(self.gallery_data['gallery_image_list'])
+        else:
+            gallery_button_list_len = max(len(displayable.item_list), 1)
+        # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
+        if not (gallery_button_list.line_item_count and gallery_button_list.line_item_count2):
+            # 横向流动，填充行，之后换行
+            if gallery_button_list.layout == 'flow_hz' :
+                gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0])
+                gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column)
+            # 纵向流动，先填充列，之后换列
+            elif gallery_button_list.layout == 'flow_vt' :
+                gallery_button_list_row = math.floor(gallery_button_list.size[1] / gallery_button_list_item.size[1])
+                gallery_button_list_column = math.ceil(gallery_button_list_len / gallery_button_list_row)
+        else:
+            gallery_button_list_column = gallery_button_list.line_item_count
+            gallery_button_list_row = gallery_button_list.line_item_count2
 
         self.reset_indent_level()
         self.screen_definition_head.append("# CG图鉴界面")
@@ -1070,10 +1089,14 @@ def generate_gallery_screen(self, component : FguiComponent):
         # 固定可拖拽
         gallery_button_list_code.append(f"{self.indent_str}draggable True")
         gallery_button_list_code.append(f"{self.indent_str}mousewheel True")
+        # 鼠标放在viewport边缘自动滚动
+        gallery_button_list_code.append(f"{self.indent_str}edgescroll (100, 200)")
         gallery_button_list_code.append(f"{self.indent_str}grid gallery_view_column gallery_view_row:")
         self.indent_level_up()
         gallery_button_list_code.append(f"{self.indent_str}yspacing {gallery_button_list.line_gap}")
         gallery_button_list_code.append(f"{self.indent_str}xspacing {gallery_button_list.col_gap}")
+        if gallery_button_list.layout == 'flow_vt' :
+            gallery_button_list_code.append(f"{self.indent_str}transpose True")
         # 若ui_helper.rpy不存在，则根据列表长度添加对应数量的默认按钮
         if not ui_helper_exists:
             for i in range(len(gallery_button_list.item_list)):
@@ -1126,11 +1149,120 @@ def generate_music_room_screen(self, component : FguiComponent):
         self.screen_ui_code.clear()
         self.screen_has_dismiss = False
         self.dismiss_action_list.clear()
+        self.music_room_screen_code.clear()
+
+        musicroom_button_list = None
+        musicroom_button_list_item = None
+        musicroom_button_list_index = -1
+        displayable_list_len = len(component.display_list.displayable_list)
+        musicroom_button_list_len = 1
+        musicroom_button_list_column =  1
+        musicroom_button_list_row = 1
+        ui_helper_exists = False
+        # 检查 gallery_data 是否存在gallery_image_list
+        if 'gallery_music_list' in self.gallery_data:
+            ui_helper_exists = True
+        else:
+            print(f"Gallery music list is not defined in {self.ui_helper_file_name}.")
 
-        if not self.gallery_template_dict['music_room']:
-            print("Music room template is empty.")
+        for i in range(displayable_list_len):
+            displayable = component.display_list.displayable_list[i]
+            # 搜索名为 musicroom_button_list 的列表组件
+            if displayable.name == 'musicroom_button_list' and isinstance(displayable, FguiList):
+                musicroom_button_list = displayable
+                # 确认默认引用组件类型。
+                musicroom_button_list_item = self.fgui_assets.get_component_by_id(musicroom_button_list.default_item_id)
+                musicroom_button_list_index = i
+                break
+        if not musicroom_button_list:
+            print(f"{component.name} contains no musicroom button list.")
+            return
+        # 检查musicroom_button_list_item是否为按钮
+        if not isinstance(musicroom_button_list_item, FguiButton):
+            print(f"{component.name} musicroom button list item is not Button.")
             return
 
+        # 计算列表的行数与列数
+        if ui_helper_exists:
+            musicroom_button_list_len = len(self.gallery_data['gallery_music_list'])
+        else:
+            musicroom_button_list_len = max(len(displayable.item_list), 1)
+        # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
+        if not (musicroom_button_list.line_item_count and musicroom_button_list.line_item_count2):
+            # 横向流动，填充行，之后换行
+            if musicroom_button_list.layout == 'flow_hz' :
+                musicroom_button_list_column = math.floor(musicroom_button_list.size[0] / musicroom_button_list_item.size[0])
+                musicroom_button_list_row = math.ceil(musicroom_button_list_len / musicroom_button_list_column)
+            # 纵向流动，先填充列，之后换列
+            elif musicroom_button_list.layout == 'flow_vt' :
+                musicroom_button_list_row = math.floor(musicroom_button_list.size[1] / musicroom_button_list_item.size[1])
+                musicroom_button_list_column = math.ceil(musicroom_button_list_len / musicroom_button_list_row)
+        else:
+            musicroom_button_list_column = musicroom_button_list.line_item_count
+            musicroom_button_list_row = musicroom_button_list.line_item_count2
+
+        self.reset_indent_level()
+        self.screen_definition_head.append("# 音乐室界面")
+        self.screen_definition_head.append(f"screen {component.name}():")
+        self.indent_level_up()
+        self.screen_definition_head.append(f"{self.indent_str}tag menu")
+        self.screen_definition_head.append(f"{self.indent_str}")
+
+        # musicroom_button_list 之前的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=musicroom_button_list_index))
+
+        # musicroom_button_list 的处理
+        musicroom_button_list_code = []
+        musicroom_button_list_code.append(f"{self.indent_str}viewport:")
+        self.indent_level_up()
+        musicroom_button_list_code.append(f"{self.indent_str}pos {musicroom_button_list.xypos}")
+        musicroom_button_list_code.append(f"{self.indent_str}xysize {musicroom_button_list.size}")
+        # 固定可拖拽
+        musicroom_button_list_code.append(f"{self.indent_str}draggable True")
+        musicroom_button_list_code.append(f"{self.indent_str}mousewheel True")
+        # 鼠标放在viewport边缘自动滚动
+        musicroom_button_list_code.append(f"{self.indent_str}edgescroll (100, 200)")
+        musicroom_button_list_code.append(f"{self.indent_str}grid musicroom_view_column musicroom_view_row:")
+        self.indent_level_up()
+        musicroom_button_list_code.append(f"{self.indent_str}yspacing {musicroom_button_list.line_gap}")
+        musicroom_button_list_code.append(f"{self.indent_str}xspacing {musicroom_button_list.col_gap}")
+        if musicroom_button_list.layout == 'flow_vt' :
+            musicroom_button_list_code.append(f"{self.indent_str}transpose True")
+        # 若ui_helper.rpy不存在，则根据列表长度添加对应数量的默认按钮
+        if not ui_helper_exists:
+            for i in range(len(musicroom_button_list.item_list)):
+                musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}()")
+        # 若ui_helper.rpy存在，则使用固定脚本定义的按钮
+        else:
+            musicroom_button_list_code.append(f"{self.indent_str}for i in range(musicroom_button_num):")
+            self.indent_level_up()
+            musicroom_button_list_code.append(f"{self.indent_str}$ name, file = gallery_music_list[i]")
+            musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}(title=name, actions=SwitchMusicRoomPlay(file)))")
+            self.indent_level_down()
+        self.screen_ui_code.extend(musicroom_button_list_code)
+        self.indent_level_down(2)
+        # musicroom_button_list 之后的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=musicroom_button_list_index+1))
+        self.screen_ui_code.append("")
+        self.reset_indent_level()
+
+        self.music_room_screen_code.extend(self.screen_definition_head)
+        self.music_room_screen_code.extend(self.screen_ui_code)
+        # 读取 templates/renpy_music_room_template.txt 模板内容，并替换 {music_room_screen_code} 为 self.music_room_screen_code
+        music_room_template_content = self.get_template_content('renpy_music_room_template.txt')
+        music_room_template_content = music_room_template_content.replace('{music_room_screen_code}', '\n'.join(self.music_room_screen_code))
+        music_room_template_content = music_room_template_content.replace('{musicroom_button_list_len}', str(musicroom_button_list_len))
+        music_room_template_content = music_room_template_content.replace('{musicroom_button_list_column}', str(musicroom_button_list_column))
+        music_room_template_content = music_room_template_content.replace('{musicroom_button_list_row}', str(musicroom_button_list_row))
+        self.music_room_screen_code = music_room_template_content.split('\n')
+
+        # 将内容保存在 game/scripts/music_room_screen.rpy 文件中
+        music_room_screen_file = os.path.join(self.game_dir, 'scripts', 'music_room_screen.rpy')
+        self.save_code_to_file(music_room_screen_file, self.music_room_screen_code)
+        print(f"Music room screen code has been saved to {music_room_screen_file}.")
+        return
+
+
     def generate_say_screen(self, component : FguiComponent):
         print("This is say screen.")
         self.screen_definition_head.clear()
@@ -1707,7 +1839,7 @@ def generate_text_displayable_string(self, fgui_text):
         text_displayable_string = f"Text(text='{fgui_text.text}',{text_anchor_param},{text_transformanchor},{text_pos_param},{text_size_param},{text_font_param},{text_font_size_param},{text_font_color_param},{text_min_width_param},{text_textalign_param},{text_bold_param},{text_italic_param},{text_underline_param},{text_strike_param},{text_outlines_parame})"
         return text_displayable_string
 
-    def generate_image_displayable(self, fgui_image : FguiImage):
+    def generate_image_displayable(self, fgui_image : FguiImage) -> list:
         """
         生成图片组件。
         前提为image对象的定义已经在generate_image_definitions中生成。
@@ -1738,7 +1870,7 @@ def generate_image_displayable(self, fgui_image : FguiImage):
                 # Ren'Py中旋转轴心固定为图片中心(0.5,0.5)或与锚点一致，锚点可指定为任意值。
                 # 若要与FairyGUI资源保持一致，需设置offset。
                 # size可能为None，需要获取
-                if not fgui_image.size:
+                if (not fgui_image.size) or (fgui_image.size == (0, 0)):
                     size = self.fgui_assets.get_image_size_by_id(fgui_image.src)
                 else:
                     size = fgui_image.size
@@ -2157,6 +2289,11 @@ def generate_list_displayable(self, fgui_list):
 
     def generate_renpy_code(self):
         """生成完整的Ren'Py代码"""
+
+
+        # 读取01_ui_helper.rpy文件
+        self.gallery_data = self.get_gallery_data_from_ui_helper(os.path.join(self.game_dir, self.ui_helper_file_name))
+        print(f"gallery_data: {self.gallery_data}")
         self.renpy_code = []
 
         # 添加文件头注释
@@ -2235,6 +2372,67 @@ def get_template_content(self, filename : str, path : str = None) -> str:
             content = file.read()
         return content
 
+    def get_gallery_data_from_ui_helper(self, file_path: str) -> dict:
+        """
+        从01_ui_helper.rpy文件中读取以"define"开头的列表
+        """
+        result = {}
+        
+        if not os.path.exists(file_path):
+            print(f"文件不存在: {file_path}")
+            return result
+        
+        try:
+            with open(file_path, 'r', encoding='utf-8') as f:
+                content = f.read()
+            
+            # 使用正则表达式匹配 define 语句
+            # 匹配格式: define variable_name = [...]
+            # 需要处理多行列表，通过匹配括号来找到完整的列表
+            pattern = r'define\s+(\w+)\s*=\s*(\[)'
+            
+            matches = list(re.finditer(pattern, content))
+            
+            for match in matches:
+                var_name = match.group(1)
+                list_start_pos = match.end() - 1  # '[' 的位置
+                
+                # 从 '[' 开始，找到匹配的 ']'
+                bracket_count = 0
+                i = list_start_pos
+                list_end_pos = -1
+                
+                while i < len(content):
+                    if content[i] == '[':
+                        bracket_count += 1
+                    elif content[i] == ']':
+                        bracket_count -= 1
+                        if bracket_count == 0:
+                            list_end_pos = i
+                            break
+                    i += 1
+                
+                if list_end_pos == -1:
+                    print(f"无法找到 {var_name} 列表的结束位置")
+                    continue
+                
+                # 提取列表字符串
+                list_str = content[list_start_pos:list_end_pos + 1]
+                
+                try:
+                    # 使用ast.literal_eval安全地解析列表
+                    list_value = ast.literal_eval(list_str)
+                    result[var_name] = list_value
+                    print(f"成功读取 define {var_name}: {len(list_value)} 个元素")
+                except (ValueError, SyntaxError) as e:
+                    print(f"解析 {var_name} 时出错: {e}")
+                    continue
+                    
+        except Exception as e:
+            print(f"读取文件时出错: {e}")
+        
+        return result
+
     def cleanup(self):
         """
         清理转换器资源
@@ -2397,12 +2595,13 @@ def convert(argv):
         # self.renpy_code.extend(self.graph_definition_code)
         # self.renpy_code.extend(self.style_code)
         converter.save_code_to_file(global_variables_output_file, converter.game_global_variables_code)
+        converter.image_definition_code.extend(converter.graph_definition_code)
         converter.save_code_to_file(image_definition_output_file, converter.image_definition_code)
         converter.save_code_to_file(style_output_file, converter.style_code)
         converter.save_code_to_file(screen_output_file, converter.screen_code)
 
         # 部分预定义模板文件修改参数并保存
-        font_map_definition_file = os.path.join(scripts_dir, "font_map.rpy")
+        font_map_definition_file = os.path.join(scripts_dir, "01_font_map.rpy")
         converter.from_templates_to_renpy(font_map_definition_file)
 
         # 复制预定义cdd和cds文件
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt
index f19f179..8c120d8 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_gallery_template.txt
@@ -21,7 +21,6 @@ init python:
             g.image(e)
             g.unlock(e)
             g.transform(gallery_full_screen)
-    # gallery_button_num = len(gallery_image_list)
     gallery_button_num = {gallery_button_list_len}
     g.transition = None
     gallery_view_column = {gallery_button_list_column}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt
new file mode 100644
index 0000000..fe71bfe
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_music_room_template.txt
@@ -0,0 +1,29 @@
+init python:
+
+    fadeout_time = 1.0
+    class SwitchMusicRoomPlay(Action):
+        def __init__(self, file, **kwargs):
+            global fadeout_time
+            super(Action, self).__init__(**kwargs)
+            self.file = file
+            self.selected = self.get_selected()
+            self.fadeout_time = fadeout_time
+        def __call__(self):
+            if self.selected:
+                renpy.music.stop("music", fadeout=self.fadeout_time)
+                self.selected = not self.selected
+            else:
+                renpy.music.play(self.file, fadeout=self.fadeout_time)
+        def get_selected(self):
+            return renpy.music.get_playing("music") == self.file
+        def periodic(self, st):
+            if self.selected != self.get_selected():
+                self.selected = self.get_selected()
+                renpy.restart_interaction()
+            return .1
+
+    musicroom_button_num = {musicroom_button_list_len}
+    musicroom_view_column = {musicroom_button_list_column}
+    musicroom_view_row = {musicroom_button_list_row}
+
+{music_room_screen_code}
\ No newline at end of file

From abed545b1708deff6c520e33930801b3b3a578a8 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sat, 24 Jan 2026 15:57:03 +0800
Subject: [PATCH 20/40] Implementation converting of ScrollBar

1. Restructure of "List" converting method.
2. Implementation converting of "scrollbar". All lists (not fgui components) can scroll with scrollbars.
3. Some bug fixed.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  25 +-
 .../utils/renpy/Fgui2RenpyConverter.py        | 818 ++++++++++--------
 .../renpy_list_screen_template.txt            |  75 ++
 .../renpy_style_preset_template.txt           |   3 +
 4 files changed, 556 insertions(+), 365 deletions(-)
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_style_preset_template.txt

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index c03224f..d614fb5 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -742,6 +742,7 @@ class FguiList(FguiDisplayable):
     overflow-溢出处理：(visible)默认-可见，hidden-隐藏，scroll-滚动
     scroll-滚动条方向：(vertical)默认-垂直滚动，horizontal-水平滚动，both-自由滚动(同时允许垂直滚动和水平滚动)
     scrollBar-显示滚动条：visible-可见，hidden-隐藏，auto-滚动时显示
+    scrollBarRes-滚动条资源url，格式为“ui://”+“packageDescription id” + “component id”。
     scrollBarFlags：一系列滚动条标识位，可能是一个12bit整数。从低位到高位分别为：
         0-bit：垂直滚动条显示在左边
         1-bit：滚动位置自动贴近元件
@@ -769,9 +770,11 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.layout = self.display_item_tree.get("layout", "column")
         self.overflow = self.display_item_tree.get("overflow", "visible")
         self.scroll = self.display_item_tree.get("scroll", "vertical")
+        scrollbar_res = self.display_item_tree.get("scrollBarRes")
+        self.scrollbar_res = self.get_scrollbar_res(scrollbar_res, package_description_id)
         self.scroll_bar_flags = int(self.display_item_tree.get("scrollBarFlags", "256"))
         margin = self.display_item_tree.get("margin")
-        self.margin = tuple(map(int, margin.split(","))) if margin else None
+        self.margin = tuple(map(int, margin.split(","))) if margin else (0,0,0,0)
         self.clip_softness = self.display_item_tree.get("clipSoftness", "0,0")
         self.line_item_count = int(self.display_item_tree.get("lineItemCount", "0"))
         self.line_item_count2 = int(self.display_item_tree.get("lineItemCount2", "0"))
@@ -786,10 +789,30 @@ def __init__(self, display_item_tree, package_description_id=None):
         for item_tree in display_item_tree:
             item = FguiListItem(item_tree, self.package_description_id)
             self.item_list.append(item)
+        self.item_list_length = 0
 
     def get_default_item(self, packageDescription_id):
         self.default_item_id = self.default_item_url[self.default_item_url.find(packageDescription_id)+len(packageDescription_id):]
 
+    def set_item_list_length(self, item_list_length):
+        self.item_list_length = item_list_length
+
+    def get_item_list_length(self):
+        return self.item_list_length if self.item_list_length > 0 else len(self.item_list)
+
+    def get_scrollbar_res(self, scrollbar_res, packageDescription_id):
+        if scrollbar_res:
+            # 原始scrollBarRes为一个使用逗号分隔的 fgui 资源url列表，总共有2项。
+            # 第1项是垂直滚动条，第2项是水平滚动条。url格式为“ui://”+“packageDescription id” + “component id”。
+            scrollbar_urls = scrollbar_res.split(",")
+            vertical_scrollbar_url = scrollbar_urls[0]
+            horizontal_scrollbar_url = scrollbar_urls[1]
+            vertical_scrollbar_id = vertical_scrollbar_url[vertical_scrollbar_url.find(packageDescription_id)+len(packageDescription_id):] if vertical_scrollbar_url else None
+            horizontal_scrollbar_id = horizontal_scrollbar_url[horizontal_scrollbar_url.find(packageDescription_id)+len(packageDescription_id):] if horizontal_scrollbar_url else None
+            return (vertical_scrollbar_id, horizontal_scrollbar_id)
+        else:
+            return None
+
 
 class FguiLoader(FguiDisplayable):
     """
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 10f0e6a..0ea94e0 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -45,6 +45,9 @@ class FguiToRenpyConverter:
     # 默认背景色，在某些未指定image的情况下用作填充。
     default_background = '#fff'
 
+    # 滚动条默认样式。empty 的效果为不显示。
+    default_scrollbar_style = 'empty'
+
     # 菜单类界面名称，需要添加 tag menu。
     menu_screen_name_list = ['main_menu', 'game_menu', 'save', 'load', 'preferences', 'history', 'help', 'about']
 
@@ -94,6 +97,7 @@ def __init__(self, fgui_assets):
         # dismiss用于取消一些组件的focus状态，例如input。
         self.screen_has_dismiss = False
         self.dismiss_action_list = []
+        self.current_component_name = None
 
         # 字体名称列表。SourceHanSansLite为Ren'Py默认字体。
         self.font_name_list = ["SourceHanSansLite"]
@@ -115,13 +119,22 @@ def __init__(self, fgui_assets):
         self.gallery_template_dict = {}
         self.gallery_template_dict['gallery'] = self.get_template_content('renpy_gallery_template.txt')
         self.gallery_template_dict['music_room'] = self.get_template_content('renpy_music_room_template.txt')
+        self.style_preset = ''
+        self.style_preset_template = 'renpy_style_preset_template.txt'
+        self.style_preset = self.get_template_content(self.style_preset_template)
+        self.list_screen_template = self.get_template_content('renpy_list_screen_template.txt')
 
         # 输出游戏目录
         self.game_dir = None
+        self.scripts_dir = None
+        self.images_dir = None
 
         # gallery相关数据
         self.gallery_data = {}
 
+        # 一些样式预设，用于覆盖Ren'Py默认样式。
+        self.style_code.append(self.style_preset)
+
     def set_game_global_variables(self, variable_name, variable_value):
         variable_str = f"define {variable_name} = {variable_value}"
         self.game_global_variables_code.append(variable_str)
@@ -276,7 +289,7 @@ def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : st
 
         return graph_code
 
-    def generate_slider_style(self, fgui_slider : FguiSlider):
+    def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
         """
         生成滑块样式。
         Ren'Py生成的滑动条效果与FGUI略有不同。
@@ -308,7 +321,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
         slider_style_code = []
         if not isinstance(fgui_slider, FguiSlider):
             print("It is not a slider.")
-            return slider_style_code
+            return
 
         # 默认为水平滑块。实际需要根据grip中relation的sidePair属性来确定。
         slider_type = BarOrientationType.HORIZONTAL
@@ -339,7 +352,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
                     second_bar_name = f"{fgui_slider.name}_{displayable.id}"
                 else:
                     print("Slider base is neither image nor graph.")
-                    return slider_style_code
+                    return
             # FGUI中滑动条的可变bar部分
             if displayable.name == 'bar':
                 if isinstance(displayable, FguiImage):
@@ -349,7 +362,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
                     first_bar_name = f"{fgui_slider.name}_{displayable.id}"
                 else:
                     print("Slider bar is neither image nor graph.")
-                    return slider_style_code
+                    return
                 bar_id = displayable.id
             # FGUI中滑动条的标题类型文本
             if displayable.name == 'title':
@@ -358,7 +371,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
                     self.generate_text_style(displayable, style_name)
                 else:
                     print("Slider title is not text.")
-                    return slider_style_code
+                    return
             # FGUI中滑动条的滑块按钮
             if displayable.name == 'grip':
                 grip_com = self.fgui_assets.get_component_by_id(displayable.src)
@@ -374,7 +387,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
                         slider_type = BarOrientationType.VERTICAL
                 else:
                     print("Slider grp is not button.")
-                    return slider_style_code
+                    return
 
         # 生成bar和thumb的image
         bar_image_definition_code.append(f"image {fgui_slider.name}_base_bar_image:")
@@ -411,6 +424,93 @@ def generate_slider_style(self, fgui_slider : FguiSlider):
 
         self.style_code.extend(slider_style_code)
 
+    def generate_scroll_bar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
+        """
+        生成滚动条样式。
+        目标样例：
+        style vertical_scrollbar:
+            bar_vertical True
+            bar_invert True
+            xsize 12
+            base_bar Frame("vertical_scrollbar_bar")
+            thumb Frame("vertical_scrollbar_grip_[prefix_]background")
+        """
+        # scrollbar_image_definition_code = []
+        style_definition_code = []
+        scrollbar_style_code = []
+        if not isinstance(fgui_scrollbar, FguiScrollBar):
+            print("It is not a scrollbar.")
+            return
+
+        # 滚动条组件固定由至少两张图片(图形)和一个按钮构成。
+        # 固定包含名为bar的空组件，用于判断滚动条方向。
+        # 按钮固定名称grip，其内部图片用于Ren'Py滚动条的thumb。
+        # 其他图片组件用于Ren'Py滚动条的bar。
+        bar_displayable_list = []
+        grip_displayable = None
+        is_vertical = False
+        for displayable in fgui_scrollbar.display_list.displayable_list:
+            # FGUI中滚动条的bar部分。只包含图片组件。暂存到bar_displayable_list。
+            if displayable.name != 'bar' and displayable.name != 'grip':
+                if isinstance(displayable, FguiImage):
+                    bar_displayable_list.append((displayable, DisplayableChildType.IMAGE))
+                elif isinstance(displayable, FguiGraph):
+                    bar_displayable_list.append((displayable, DisplayableChildType.GRAPH))
+                elif isinstance(displayable, FguiText):
+                    bar_displayable_list.append((displayable, DisplayableChildType.TEXT))
+                else:
+                    print("Scrollbar bar has non-image, graph or text component.")
+                    return
+            # FGUI中滚动条的按钮
+            if displayable.name == 'grip':
+                grip_displayable = displayable
+            # 根据bar部分判断滚动条方向
+            if displayable.name == 'bar' and isinstance(displayable, FguiGraph):
+                # 根据relation中的sidePair属性判断滚动条方向
+                side_pair_str = displayable.relations.relation_dict['']
+                if side_pair_str == "height-height":
+                    is_vertical = True
+                else:
+                    is_vertical = False
+
+        # 检查bar_displayable_list是否为空
+        if len(bar_displayable_list) == 0:
+            print("Scrollbar has no bar.")
+            return scrollbar_style_code
+        # 检查按钮
+        if grip_displayable is None:
+            print("Scrollbar has no grip.")
+            return
+
+        # 生成bar和thumb的image定义
+        # 滚动条的bar部分为复合图片对象，包含所有bar_displayable_list中的图片。
+        # 暂时不处理图片的transform。
+        self.generate_composite_image_object(f"{fgui_scrollbar.name}_idle_bar", bar_displayable_list, fgui_scrollbar.name, fgui_scrollbar.size)
+
+        # 获取按钮的实际引用组件
+        grip_com = self.fgui_assets.get_component_by_id(grip_displayable.src)
+        if isinstance(grip_com, FguiButton):
+            # grip按钮的图片定义已在其他地方生成，此处指需要获取其图片名。
+            thumb_image_name = f"{fgui_scrollbar.name}_grip_[prefix_]background"
+        else:
+            print("Scrollbar grip is not button.")
+            return
+            
+        # 生成scrollbar样式
+        scrollbar_style_code.append(f"style {fgui_scrollbar.name}:")
+        scrollbar_style_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
+        scrollbar_style_code.append(f"{self.indent_unit}bar_invert {is_vertical}")
+        scrollbar_style_code.append(f"{self.indent_unit}xysize {fgui_scrollbar.size}")
+        scrollbar_style_code.append(f"{self.indent_unit}base_bar Frame('{fgui_scrollbar.name}_[prefix_]bar')")
+        scrollbar_style_code.append(f"{self.indent_unit}thumb Frame('{thumb_image_name}')")
+        scrollbar_style_code.append("")
+
+        # 添加头部注释
+        # scrollbar_style_code.extend(scrollbar_image_definition_code)
+        scrollbar_style_code.append("# 滚动条样式定义")
+        scrollbar_style_code.extend(style_definition_code)
+
+        self.style_code.extend(scrollbar_style_code)
 
     @staticmethod
     def is_menu_screen(screen_name : str):
@@ -459,7 +559,7 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                 screen_ui_code.extend(self.generate_image_displayable(displayable))
             # 图形组件
             elif isinstance(displayable, FguiGraph):
-                screen_ui_code.extend(self.generate_graph_displayable(displayable, component.name))
+                screen_ui_code.extend(self.generate_graph_displayable(displayable))
             # 文本组件
             elif isinstance(displayable, FguiText):
                 screen_ui_code.extend(self.generate_text_displayable(displayable))
@@ -566,12 +666,14 @@ def generate_screen(self, component : FguiComponent):
         self.screen_ui_code.clear()
         self.screen_has_dismiss = False
         self.dismiss_action_list.clear()
+        choice_screen_code = []
 
         self.screen_definition_head.append("# 界面定义")
         self.screen_definition_head.append(f"# 从FairyGUI组件{component.name}转换而来")
 
         id = component.id
         screen_name = component.name
+        self.current_component_name = screen_name
 
         # 界面入参列表
         screen_params = ''
@@ -698,38 +800,40 @@ def generate_choice_screen(self, component : FguiComponent):
             print("Choice button list's item is not Button.")
             return
 
+        choice_screen_code = []
+        screen_params = 'items'
+        choice_screen_code.append(f"# 选项界面")
+        choice_screen_code.append(f"screen {component.name}({screen_params}):")
+
         # vbox的行距
         vbox_spacing = choice_list.line_gap if choice_list else 0
-        screen_params = 'items'
         self.reset_indent_level()
-        self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
         self.indent_level_up()
         # 选项菜单标题
-        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        choice_screen_code.append(f"{self.indent_str}fixed:")
         self.indent_level_up()
-        self.screen_ui_code.append(f"{self.indent_str}pos {caption_text.xypos}")
-        self.screen_ui_code.append(f"{self.indent_str}for i in items:")
+        choice_screen_code.append(f"{self.indent_str}pos {caption_text.xypos}")
+        choice_screen_code.append(f"{self.indent_str}for i in items:")
         self.indent_level_up()
-        self.screen_ui_code.append(f"{self.indent_str}if not i.action:")
+        choice_screen_code.append(f"{self.indent_str}if not i.action:")
         self.indent_level_up()
-        self.screen_ui_code.append(f"{self.indent_str}text i.caption style 'choice_caption_text_style'")
+        choice_screen_code.append(f"{self.indent_str}text i.caption style 'choice_caption_text_style'")
         self.indent_level_down(3)
         # 选项菜单按钮列表
-        self.screen_ui_code.append(f"{self.indent_str}vbox:")
+        choice_screen_code.append(f"{self.indent_str}vbox:")
         self.indent_level_up()
-        self.screen_ui_code.append(f"{self.indent_str}spacing {vbox_spacing}")
-        self.screen_ui_code.append(f"{self.indent_str}pos {choice_list.xypos}")
-        self.screen_ui_code.append(f"{self.indent_str}for i in items:")
+        choice_screen_code.append(f"{self.indent_str}spacing {vbox_spacing}")
+        choice_screen_code.append(f"{self.indent_str}pos {choice_list.xypos}")
+        choice_screen_code.append(f"{self.indent_str}for i in items:")
         self.indent_level_up()
-        self.screen_ui_code.append(f"{self.indent_str}if i.action:")
+        choice_screen_code.append(f"{self.indent_str}if i.action:")
         self.indent_level_up()
         parameter_str = f"title=i.caption, actions=i.action"
-        self.screen_ui_code.append(f"{self.indent_str}use {choice_button.name}({parameter_str})")
+        choice_screen_code.append(f"{self.indent_str}use {choice_button.name}({parameter_str})")
         self.reset_indent_level()
 
-        self.screen_ui_code.append("")
-        self.screen_code.extend(self.screen_definition_head)
-        self.screen_code.extend(self.screen_ui_code)
+        choice_screen_code.append("")
+        self.screen_code.extend(choice_screen_code)
         return
 
     def generate_save_load_screen(self, component : FguiComponent):
@@ -743,6 +847,7 @@ def generate_save_load_screen(self, component : FguiComponent):
         slot_list = None
         slot_list_index = -1
         default_slot_button = None
+        save_slot_list_id = None
         if component.display_list.displayable_list == None:
             print(f"{component.name} contains no displayable.")
             return
@@ -763,7 +868,8 @@ def generate_save_load_screen(self, component : FguiComponent):
             print(f"{component.name} slot list item is not Button.")
             return
         
-        self.screen_definition_head.append("# 存档/读档 界面")
+        comment_screen_name = "存档" if component.name == "save" else "读档"
+        self.screen_definition_head.append(f"# {comment_screen_name} 界面")
         self.screen_definition_head.append(f"screen {component.name}():")
 
         # 菜单标签
@@ -773,85 +879,22 @@ def generate_save_load_screen(self, component : FguiComponent):
 
         # save_slot_list的处理
         slot_list_code = []
-        item_number = len(slot_list.item_list)
-
-        # 根据“溢出处理”是否可见区分处理。
-        # 若“可见”，则使用hbox、vbox和grid。
-        if slot_list.overflow == "visible":
-            # 单列竖排，使用vbox
-            if slot_list.layout == "column":
-                slot_list_code.append(f"{self.indent_str}vbox:")
-            # 单行横排，使用hbox
-            elif slot_list.layout == "row":
-                slot_list_code.append(f"{self.indent_str}hbox:")
-            # 其他，使用grid
-            else:
-                slot_list_code.append(f"{self.indent_str}grid {slot_list.line_item_count} {slot_list.line_item_count2}:")
 
-        else:
-            slot_list_code.append(f"{self.indent_str}vpgrid:")
-            self.indent_level_up()
-
-            # 若“隐藏”，使用不可滚动的vpgrid
-            if slot_list.overflow == "hidden":
-                slot_list_code.append(f"{self.indent_str}draggable False")
-                # 单列竖排
-                if slot_list.layout == "column":
-                    cols = 1
-                    rows = item_number
-                # 单行横排
-                elif slot_list.layout == "row":
-                    cols = item_number
-                    rows = 1
-                # 其他
-                else:
-                    cols = slot_list.line_item_count
-                    rows = slot_list.line_item_count2
-                slot_list_code.append(f"{self.indent_str}cols {cols}")
-                slot_list_code.append(f"{self.indent_str}rows {rows}")
-            # 若“滚动”，使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。
-            elif slot_list.overflow == "scroll":
-                slot_list_code.append(f"{self.indent_str}draggable True")
-                # 垂直滚动
-                if slot_list.scroll == "vertical":
-                    pass
-                # 水平滚动
-                elif slot_list.scroll == "horizontal":
-                    pass
-                # 自由滚动
-                elif slot_list.scroll == "both":
-                    pass
-                # 单列竖排，使用vbox
-                if slot_list.layout == "column":
-                    cols = 1
-                    rows = item_number
-                # 单行横排，使用hbox
-                elif slot_list.layout == "row":
-                    cols = item_number
-                    rows = 1
-                # 其他，使用grid
-                else:
-                    cols = slot_list.line_item_count
-                    rows = slot_list.line_item_count2
-                slot_list_code.append(f"{self.indent_str}cols {cols}")
-                slot_list_code.append(f"{self.indent_str}rows {rows}")
-                if slot_list.line_gap:
-                    slot_list_code.append(f"{self.indent_str}yspacing {slot_list.line_gap}")
-                if slot_list.col_gap:
-                    slot_list_code.append(f"{self.indent_str}xspacing {slot_list.col_gap}")
-            self.indent_level_down()
+        list_length = len(slot_list.item_list)
 
-        self.indent_level_up()
-        slot_list_code.append(f"{self.indent_str}pos {slot_list.xypos}")
-        slot_list_code.append(f"{self.indent_str}xysize {slot_list.size}")
-        if slot_list.margin:
-            slot_list_code.append(f"{self.indent_str}margin {slot_list.margin}")
+        screen_params = self.generate_list_screen_params(slot_list)
+        if not screen_params:
+            print("Failed to generate slot list screen.")
+            return
+        # 引用列表组件的screen。
+        component_name = component.name
+        slot_list_code.append(f"{self.indent_str}use {component_name}_{slot_list.name}({screen_params}):")
         # 添加元素
-        for i in range(item_number):
+        self.indent_level_up()
+        for i in range(list_length):
             # 值根据列表长度添加对应数量的默认元素，即存档按钮
             parameter_str = self.generate_button_callable_parameter(f"FileTime({i+1}, format=_(\"{{#file_time}}%Y-%m-%d %H:%M\"), empty=_(''))", f"FileAction({i+1})", f"FileScreenshot({i+1})")
             slot_list_code.append(f"{self.indent_str}use {default_slot_button.name}({parameter_str})")
-
         self.indent_level_down()
         self.screen_ui_code.extend(slot_list_code)
 
@@ -863,6 +906,123 @@ def generate_save_load_screen(self, component : FguiComponent):
         self.screen_code.extend(self.screen_ui_code)
         return
 
+    def generate_list_screen_params(self, fgui_list : FguiList) -> str:
+        """
+        生成列表组件screen，并返回引用该screen的参数字符串。
+        """
+        if not isinstance(fgui_list, FguiList):
+            print("It is not a list displayable.")
+            return ''
+        component_name = self.current_component_name
+
+        list_result = self.generate_list_screen(fgui_list, component_name)
+        if not list_result:
+            print(f"Failed to generate {component_name}_{fgui_list.name} list screen.")
+            return ''
+
+        list_length = fgui_list.get_item_list_length()
+        default_item = self.fgui_assets.get_component_by_id(fgui_list.default_item_id)
+        default_item_type = None
+        default_item_name = None
+        default_item_size = (0, 0)
+        if default_item:
+            default_item_name = default_item.name
+            default_item_type = default_item.extention
+            default_item_size = default_item.size
+        else:
+            default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id)
+            if default_item_name:
+                default_item_type = 'image'
+                default_item_size = self.fgui_assets.get_image_size_by_id(fgui_list.default_item_id)
+            else:
+                print("Ref com not found.")
+                return ''
+        
+
+        overflow = None
+        layout = fgui_list.layout
+        row_num = 1
+        column_num = 1
+        transpose = False
+        xspacing = 0
+        yspacing = 0
+        margin = (0, 0, 0, 0)
+        xysize = (0, 0)
+        pos = (0, 0)
+
+        # 根据列表布局计算行数与列数。
+        # layout-列表布局：(column)默认-单列竖排，row-单行横排，flow_hz-横向流动，flow_vt-纵向流动、pagination-分页
+        # lineItemCount：列表布局为横向流动或分页时，表示列数。列表布局为竖向流动时，表示行数。其他布局中，该参数无效果。
+        # lineItemCount2：列表布局为分页时，表示行数。其他布局中，该参数无效果。
+        if fgui_list.layout == "column":
+            column_num = 1
+            row_num = list_length
+        elif fgui_list.layout == "row":
+            row_num = 1
+            column_num = list_length
+        elif fgui_list.layout == 'flow_hz':
+            # column_num可能为0，需要根据列表尺寸与元素尺寸计算实际的列数。
+            if fgui_list.line_item_count == 0:
+                column_num = math.floor(fgui_list.size[0] / (default_item_size[0] + fgui_list.col_gap) + 1)
+            else:
+                column_num = fgui_list.line_item_count
+            row_num = math.ceil(list_length / column_num)
+        elif fgui_list.layout == 'flow_vt':
+            # row_num可能为0，需要根据列表尺寸与元素尺寸计算实际的行数。
+            if fgui_list.line_item_count == 0:
+                row_num = math.floor(fgui_list.size[1] / (default_item_size[1] + fgui_list.line_gap) + 1)
+            else:
+                row_num = fgui_list.line_item_count
+            column_num = math.ceil(list_length / row_num)
+        else:
+            row_num = fgui_list.line_item_count
+            column_num = fgui_list.line_item_count2 + 1
+
+        xspacing = fgui_list.col_gap
+        yspacing = fgui_list.line_gap
+        transpose = (fgui_list.layout == 'flow_vt')
+        xysize = fgui_list.size
+        pos = fgui_list.xypos
+        margin = fgui_list.margin
+        screen_params = f"overflow='{overflow}', layout='{layout}', row_num={row_num}, column_num={column_num}, transpose={transpose}, xspacing={xspacing}, yspacing={yspacing}, xysize={xysize}, pos={pos}, margin={margin}"
+
+        return screen_params
+
+    def get_scrollbar_style(self, scrollbar_res : tuple[str, str]) -> tuple[str, str]:
+        """
+        根据滚动条配置，返回两个滚动条组件的样式。
+        """
+        if not scrollbar_res:
+            return (None, None)
+        vertical_scrollbar_component = self.fgui_assets.get_component_by_id(scrollbar_res[0]) if scrollbar_res[0] else None
+        horizontal_scrollbar_component = self.fgui_assets.get_component_by_id(scrollbar_res[1]) if scrollbar_res[1] else None
+
+        return (vertical_scrollbar_component, horizontal_scrollbar_component)
+
+    def get_side_parts_and_scrollbars(self, scrollbar_res : tuple[str, str]) -> tuple[str, FguiComponent, FguiComponent]:
+        """
+        根据滚动条配置，返回可视组件side的属性和两个滚动条组件。
+        若没有滚动条，可视组件side的属性 'c'。
+        若滚动条为水平方向，可视组件side的属性 'c,b'。
+        若滚动条为垂直方向，可视组件side的属性 'c,r'。
+        若两者皆有，可视组件side的属性 'c,b,r'。
+        """
+        if not scrollbar_res:
+            return (None, None, None)
+        vertical_scrollbar_component = None
+        horizontal_scrollbar_component = None
+        if scrollbar_res[0]:
+            vertical_scrollbar_component = self.fgui_assets.get_component_by_id(scrollbar_res[0])
+        if scrollbar_res[1]:
+            horizontal_scrollbar_component = self.fgui_assets.get_component_by_id(scrollbar_res[1])
+        side_parts = 'c'
+        if vertical_scrollbar_component:
+            side_parts += ' r'
+        if horizontal_scrollbar_component:
+            side_parts += ' b'
+        return (side_parts, vertical_scrollbar_component, horizontal_scrollbar_component)
+
+
     def generate_history_screen(self, component :FguiComponent):
         print("This is history screen.")
         self.screen_definition_head.clear()
@@ -876,9 +1036,13 @@ def generate_history_screen(self, component :FguiComponent):
         history_list = None
         history_list_index = -1
 
+        if component.display_list.displayable_list == None:
+            print(f"{component.name} contains no displayable.")
+            return
+
         for i in range(displayable_list_len):
             displayable = component.display_list.displayable_list[i]
-            # 搜索名为 save_slot_list 的列表组件
+            # 搜索名为 history_list 的列表组件
             if displayable.name == 'history_list' and isinstance(displayable, FguiList):
                 history_list = displayable
                 history_list_index = i
@@ -905,18 +1069,18 @@ def generate_history_screen(self, component :FguiComponent):
 
         # history_list的处理
         history_list_code = []
-        history_list_code.append(f"{self.indent_str}vpgrid:")
+        screen_params = self.generate_list_screen_params(history_list)
+        if not screen_params:
+            print("Failed to generate history list screen.")
+            return
+        # 引用列表组件的screen。
+        component_name = component.name
+        history_list_code.append(f"{self.indent_str}use {component_name}_{history_list.name}({screen_params}):")
+        # 添加元素
         self.indent_level_up()
-        # 固定可拖拽
-        history_list_code.append(f"{self.indent_str}draggable True")
-        # 固定一列
-        history_list_code.append(f"{self.indent_str}cols 1")
-        history_list_code.append(f"{self.indent_str}yspacing {history_list.line_gap}")
-        history_list_code.append(f"{self.indent_str}pos {history_list.xypos}")
-        history_list_code.append(f"{self.indent_str}xysize {history_list.size}")
         history_list_code.append(f"{self.indent_str}for h in _history_list:")
         self.indent_level_up()
-        # item组件可能包含多个子组件，直接引用会出现vpgrid overfull错误
+        # item组件可能包含多个子组件，直接引用会出现grid overfull错误
         history_list_code.append(f"{self.indent_str}fixed:")
         self.indent_level_up()
         history_list_code.append(f"{self.indent_str}xysize {history_item.size}")
@@ -943,10 +1107,10 @@ def generate_history_item(self, component : FguiComponent):
         self.dismiss_action_list.clear()
         who_text = None
         what_text = None
-        namebox = None
-        textbox = None
-        namebox_pos = (0, 0)
-        textbox_pos = (0, 0)
+        # namebox = None
+        # textbox = None
+        # namebox_pos = (0, 0)
+        # textbox_pos = (0, 0)
         who_index = 0
         what_index = 0
         
@@ -980,8 +1144,6 @@ def generate_history_item(self, component : FguiComponent):
         self.reset_indent_level()
         # say界面需要覆盖默认gui设置
         self.screen_definition_head.append("# history_item界面，用于显示一条对话记录。")
-        # self.screen_definition_head.append("style history_label is history_who_text_style")
-        # self.screen_definition_head.append("style history_dialogue is history_what_text_style")
         self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
         self.indent_level_up()
 
@@ -1051,25 +1213,6 @@ def generate_gallery_screen(self, component : FguiComponent):
             print(f"{component.name} gallery button list item is not Button.")
             return
 
-        # 计算列表的行数与列数
-        if ui_helper_exists:
-            gallery_button_list_len = len(self.gallery_data['gallery_image_list'])
-        else:
-            gallery_button_list_len = max(len(displayable.item_list), 1)
-        # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
-        if not (gallery_button_list.line_item_count and gallery_button_list.line_item_count2):
-            # 横向流动，填充行，之后换行
-            if gallery_button_list.layout == 'flow_hz' :
-                gallery_button_list_column = math.floor(gallery_button_list.size[0] / gallery_button_list_item.size[0])
-                gallery_button_list_row = math.ceil(gallery_button_list_len / gallery_button_list_column)
-            # 纵向流动，先填充列，之后换列
-            elif gallery_button_list.layout == 'flow_vt' :
-                gallery_button_list_row = math.floor(gallery_button_list.size[1] / gallery_button_list_item.size[1])
-                gallery_button_list_column = math.ceil(gallery_button_list_len / gallery_button_list_row)
-        else:
-            gallery_button_list_column = gallery_button_list.line_item_count
-            gallery_button_list_row = gallery_button_list.line_item_count2
-
         self.reset_indent_level()
         self.screen_definition_head.append("# CG图鉴界面")
         self.screen_definition_head.append(f"screen {component.name}():")
@@ -1080,28 +1223,26 @@ def generate_gallery_screen(self, component : FguiComponent):
         # gallery_button_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=gallery_button_list_index))
 
+        # 若ui_helper.rpy存在，修改 gallery_button_list 的长度。
+        if ui_helper_exists:
+            gallery_button_list_len = len(self.gallery_data['gallery_image_list'])
+            gallery_button_list.set_item_list_length(gallery_button_list_len)
         # gallery_button_list 的处理
         gallery_button_list_code = []
-        gallery_button_list_code.append(f"{self.indent_str}viewport:")
-        self.indent_level_up()
-        gallery_button_list_code.append(f"{self.indent_str}pos {gallery_button_list.xypos}")
-        gallery_button_list_code.append(f"{self.indent_str}xysize {gallery_button_list.size}")
-        # 固定可拖拽
-        gallery_button_list_code.append(f"{self.indent_str}draggable True")
-        gallery_button_list_code.append(f"{self.indent_str}mousewheel True")
-        # 鼠标放在viewport边缘自动滚动
-        gallery_button_list_code.append(f"{self.indent_str}edgescroll (100, 200)")
-        gallery_button_list_code.append(f"{self.indent_str}grid gallery_view_column gallery_view_row:")
+        screen_params = self.generate_list_screen_params(gallery_button_list)
+        if not screen_params:
+            print("Failed to generate list screen.")
+            return
+        # 引用列表组件的screen。
+        component_name = component.name
+        gallery_button_list_code.append(f"{self.indent_str}use {component_name}_{gallery_button_list.name}({screen_params}):")
         self.indent_level_up()
-        gallery_button_list_code.append(f"{self.indent_str}yspacing {gallery_button_list.line_gap}")
-        gallery_button_list_code.append(f"{self.indent_str}xspacing {gallery_button_list.col_gap}")
-        if gallery_button_list.layout == 'flow_vt' :
-            gallery_button_list_code.append(f"{self.indent_str}transpose True")
-        # 若ui_helper.rpy不存在，则根据列表长度添加对应数量的默认按钮
+
+        # 若ui_helper.rpy不存在，根据列表长度添加对应数量的默认按钮。
         if not ui_helper_exists:
             for i in range(len(gallery_button_list.item_list)):
                 gallery_button_list_code.append(f"{self.indent_str}use {gallery_button_list_item.name}()")
-        # 若ui_helper.rpy存在，则使用固定脚本定义的按钮
+        # 若ui_helper.rpy存在，则使用固定脚本定义的按钮。
         else:
             gallery_button_list_code.append(f"{self.indent_str}for i in range(gallery_button_num):")
             self.indent_level_up()
@@ -1118,7 +1259,7 @@ def generate_gallery_screen(self, component : FguiComponent):
             self.indent_level_down()
         self.screen_ui_code.extend(gallery_button_list_code)
 
-        self.indent_level_down(2)
+        self.indent_level_down()
         # gallery_button_list 之后的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=gallery_button_list_index+1))
         self.screen_ui_code.append("")
@@ -1182,25 +1323,6 @@ def generate_music_room_screen(self, component : FguiComponent):
             print(f"{component.name} musicroom button list item is not Button.")
             return
 
-        # 计算列表的行数与列数
-        if ui_helper_exists:
-            musicroom_button_list_len = len(self.gallery_data['gallery_music_list'])
-        else:
-            musicroom_button_list_len = max(len(displayable.item_list), 1)
-        # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
-        if not (musicroom_button_list.line_item_count and musicroom_button_list.line_item_count2):
-            # 横向流动，填充行，之后换行
-            if musicroom_button_list.layout == 'flow_hz' :
-                musicroom_button_list_column = math.floor(musicroom_button_list.size[0] / musicroom_button_list_item.size[0])
-                musicroom_button_list_row = math.ceil(musicroom_button_list_len / musicroom_button_list_column)
-            # 纵向流动，先填充列，之后换列
-            elif musicroom_button_list.layout == 'flow_vt' :
-                musicroom_button_list_row = math.floor(musicroom_button_list.size[1] / musicroom_button_list_item.size[1])
-                musicroom_button_list_column = math.ceil(musicroom_button_list_len / musicroom_button_list_row)
-        else:
-            musicroom_button_list_column = musicroom_button_list.line_item_count
-            musicroom_button_list_row = musicroom_button_list.line_item_count2
-
         self.reset_indent_level()
         self.screen_definition_head.append("# 音乐室界面")
         self.screen_definition_head.append(f"screen {component.name}():")
@@ -1211,36 +1333,33 @@ def generate_music_room_screen(self, component : FguiComponent):
         # musicroom_button_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=musicroom_button_list_index))
 
+        # 若ui_helper.rpy存在，修改 musicroom_button_list 的长度。
+        if ui_helper_exists:
+            musicroom_button_list_len = len(self.gallery_data['gallery_music_list'])
+            musicroom_button_list.set_item_list_length(musicroom_button_list_len)
+
         # musicroom_button_list 的处理
         musicroom_button_list_code = []
-        musicroom_button_list_code.append(f"{self.indent_str}viewport:")
-        self.indent_level_up()
-        musicroom_button_list_code.append(f"{self.indent_str}pos {musicroom_button_list.xypos}")
-        musicroom_button_list_code.append(f"{self.indent_str}xysize {musicroom_button_list.size}")
-        # 固定可拖拽
-        musicroom_button_list_code.append(f"{self.indent_str}draggable True")
-        musicroom_button_list_code.append(f"{self.indent_str}mousewheel True")
-        # 鼠标放在viewport边缘自动滚动
-        musicroom_button_list_code.append(f"{self.indent_str}edgescroll (100, 200)")
-        musicroom_button_list_code.append(f"{self.indent_str}grid musicroom_view_column musicroom_view_row:")
+        screen_params = self.generate_list_screen_params(musicroom_button_list)
+        if not screen_params:
+            print("Failed to generate music room list screen.")
+            return
+        component_name = component.name
+        musicroom_button_list_code.append(f"{self.indent_str}use {component_name}_{musicroom_button_list.name}({screen_params}):")
         self.indent_level_up()
-        musicroom_button_list_code.append(f"{self.indent_str}yspacing {musicroom_button_list.line_gap}")
-        musicroom_button_list_code.append(f"{self.indent_str}xspacing {musicroom_button_list.col_gap}")
-        if musicroom_button_list.layout == 'flow_vt' :
-            musicroom_button_list_code.append(f"{self.indent_str}transpose True")
         # 若ui_helper.rpy不存在，则根据列表长度添加对应数量的默认按钮
         if not ui_helper_exists:
-            for i in range(len(musicroom_button_list.item_list)):
+            for i in range(musicroom_button_list.get_item_list_length()):
                 musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}()")
         # 若ui_helper.rpy存在，则使用固定脚本定义的按钮
         else:
             musicroom_button_list_code.append(f"{self.indent_str}for i in range(musicroom_button_num):")
             self.indent_level_up()
             musicroom_button_list_code.append(f"{self.indent_str}$ name, file = gallery_music_list[i]")
-            musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}(title=name, actions=SwitchMusicRoomPlay(file)))")
+            musicroom_button_list_code.append(f"{self.indent_str}use {musicroom_button_list_item.name}(title=name, actions=SwitchMusicRoomPlay(file))")
             self.indent_level_down()
         self.screen_ui_code.extend(musicroom_button_list_code)
-        self.indent_level_down(2)
+        self.indent_level_down()
         # musicroom_button_list 之后的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=musicroom_button_list_index+1))
         self.screen_ui_code.append("")
@@ -1532,7 +1651,20 @@ def generate_button_children(self, fgui_button : FguiButton):
 
         return state_children_dict
 
-    def generate_button_screen(self, component):
+    @staticmethod
+    def is_bar_grip(component : FguiComponent) -> bool:
+        """
+        判断是否为滚动条的grip组件。
+        """
+        if not isinstance(component, FguiComponent):
+            return False
+        if component.name is None:
+            return False
+        if not isinstance(component.name, str):
+            return False
+        return component.name.endswith('_grip')
+
+    def generate_button_screen(self, component : FguiComponent):
 
         """
         生成按钮组件screen。目标样例：
@@ -1563,7 +1695,6 @@ def generate_button_screen(self, component):
         self.screen_ui_code.clear()
         self.has_dismiss = False
         self.dismiss_action_list.clear()
-        # self.style_code.clear()
 
         # 4种状态的子组件列表
         idle_child_list = []
@@ -1675,16 +1806,21 @@ def generate_button_screen(self, component):
                 pass
 
         # 根据state_children_dict生成各种对应状态的background
-        # idle_background
-        self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name)
+        is_bar_grip = self.is_bar_grip(component)
+        # self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name)
+        self.generate_composite_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name, xysize, is_frame=is_bar_grip)
         # selected_background
-        self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name)
+        # self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name)
+        self.generate_composite_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name, xysize, is_frame=is_bar_grip)
         # hover_background
-        self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name)
+        # self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name)
+        self.generate_composite_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name, xysize, is_frame=is_bar_grip)
         # selected_hover_background
-        self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name)
+        # self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name)
+        self.generate_composite_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name, xysize, is_frame=is_bar_grip)
         # insensitive_background
-        self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name)
+        # self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name)
+        self.generate_composite_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name, xysize, is_frame=is_bar_grip)
 
 
         # 重置缩进级别
@@ -1740,7 +1876,6 @@ def generate_button_screen(self, component):
 
         self.screen_ui_code.append("")
 
-        # self.screen_code.extend(self.style_code)
         self.screen_code.extend(self.screen_definition_head)
         self.screen_code.extend(self.screen_ui_code)
         # self.renpy_code.extend(self.screen_code)
@@ -1748,6 +1883,42 @@ def generate_button_screen(self, component):
     def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"):
         return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5)
 
+    def generate_composite_image_object(self, composite_name : str, displayable_list : list, component_name: str, size : tuple[int, int], is_frame : bool = False):
+        """
+        生成合成图片对象。
+        """
+        image_definitions = []
+        image_definitions.append("# image对象定义")
+        image_definitions.append("# 使用其他image对象的合成")
+        indent_level = self.root_indent_level
+        self.reset_indent_level()
+        image_definitions.append(f"image {composite_name}:")
+        self.indent_level_up()
+        image_definitions.append(f"{self.indent_str}Composite({size},")
+        self.indent_level_up()
+        for displayable, displayalbe_type in displayable_list:
+            image_definitions.append(f"{self.indent_str}{displayable.xypos},")
+            if displayalbe_type == DisplayableChildType.IMAGE:
+                name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                if is_frame:
+                    image_definitions.append(f"{self.indent_str}Frame('{name}'),")
+                else:
+                    image_definitions.append(f"{self.indent_str}'{name}',")
+            elif displayalbe_type == DisplayableChildType.GRAPH:
+                image_definitions.append(f"{self.indent_str}'{component_name}_{displayable.id}',")
+            elif displayalbe_type == DisplayableChildType.TEXT:
+                text_displayable_string = self.generate_text_displayable_string(displayable)
+                image_definitions.append(f"{self.indent_str}{text_displayable_string},")
+            # 其他类型暂时用空对象占位
+            else:
+                image_definitions.append(f"{self.indent_str}Null()")
+
+        image_definitions.append(")")
+        image_definitions.append("")
+
+        self.image_definition_code.extend(image_definitions)
+        self.root_indent_level = indent_level
+
     def generate_image_object(self, image_name : str, displayable_list : list, component_name: str):
         """
         生成image对象，入参为一些子组件列表，自带transform。
@@ -1898,7 +2069,7 @@ def generate_image_displayable(self, fgui_image : FguiImage) -> list:
         self.indent_level_down(end_indent_level)
         return image_code
 
-    def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : str) -> list:
+    def generate_graph_displayable(self, fgui_graph : FguiGraph) -> list:
         """
         生成图形组件。用于screen中。
         图形组件有多种类别：
@@ -1913,6 +2084,7 @@ def generate_graph_displayable(self, fgui_graph : FguiGraph, component_name : st
         if not isinstance(fgui_graph, FguiGraph):
             print("It is not a graph displayable.")
             return graph_code
+        component_name = self.current_component_name
 
         self.graph_definition_code.extend(self.generate_graph_definitions(fgui_graph, component_name))
         graph_code.append(f"{self.indent_str}add '{component_name}_{fgui_graph.id}'")
@@ -2085,7 +2257,7 @@ def generate_text_displayable(self, fgui_text):
         self.indent_level_down(end_indent_level)
         return text_code
 
-    def generate_list_displayable(self, fgui_list):
+    def generate_list_displayable(self, fgui_list : FguiList) -> list[str]:
         """
         生成列表。
         """
@@ -2093,16 +2265,15 @@ def generate_list_displayable(self, fgui_list):
         if not isinstance(fgui_list, FguiList):
             print("It is not a list displayable.")
             return list_code
+        component_name = self.current_component_name
 
         # 默认引用组件可能是图片或其他组件，后续处理方式不同。
         default_item = self.fgui_assets.get_component_by_id(fgui_list.default_item_id)
         default_item_type = None
         default_item_name = None
-        item_number = len(fgui_list.item_list)
         # 若为组件
         if default_item:
-            default_item_name = self.fgui_assets.get_componentname_by_id(fgui_list.default_item_id)
-            # default_item_type = 'component'
+            default_item_name = default_item.name
             default_item_type = default_item.extention
         # 若非组件
         else:
@@ -2113,157 +2284,57 @@ def generate_list_displayable(self, fgui_list):
                 print("Ref com not found.")
                 return list_code
 
-        # 列表与默认元素尺寸，可能用于grid或vpgrid的行数与列数计算
-        list_xysize = fgui_list.size
-        default_item_size = default_item.size
-        list_length = len(fgui_list.item_list)
-        column_num = 1
-        row_num = 1
-        end_indent_level = 1
-
-        # 根据“溢出处理”是否可见区分处理。
-        # 若“可见”，则使用hbox、vbox和grid。
-        if fgui_list.overflow == "visible":
-            # 单列竖排，使用vbox
-            if fgui_list.layout == "column":
-                list_code.append(f"{self.indent_str}vbox:")
-                self.indent_level_up()
-                if fgui_list.line_gap:
-                    list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}")
-
-            # 单行横排，使用hbox
-            elif fgui_list.layout == "row":
-                list_code.append(f"{self.indent_str}hbox:")
-                self.indent_level_up()
-                if fgui_list.line_gap:
-                    list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}")
-
-            # 其他，使用grid
-            else:
-                # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
-                if not (fgui_list.line_item_count and fgui_list.line_item_count2):
-                    # 横向流动，填充行，之后换行
-                    if fgui_list.layout == 'flow_hz' :
-                        column_num = math.floor(list_xysize[0] / default_item_size[0])
-                        row_num = math.ceil(list_length / column_num)
-                    # 纵向流动，先填充列，之后换列
-                    elif fgui_list.layout == 'flow_vt' :
-                        row_num = math.floor(list_xysize[1] / default_item_size[1])
-                        column_num = math.ceil(list_length / row_num)
-                    list_code.append(f"{self.indent_str}grid {column_num} {row_num}:")
-                else:
-                    list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
-                self.indent_level_up()
-                if fgui_list.layout == 'flow_vt' :
-                    list_code.append(f"{self.indent_str}transpose True")
-                if fgui_list.line_gap:
-                    list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
-                if fgui_list.col_gap:
-                    list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
-            list_code.append(f"{self.indent_str}pos {fgui_list.xypos}")
-            list_code.append(f"{self.indent_str}xysize {fgui_list.size}")
+        list_length = fgui_list.get_item_list_length()
 
+        # 生成列表组件的screen。
+        list_result = self.generate_list_screen(fgui_list, component_name)
+        if not list_result:
+            print(f"Failed to generate {component_name}_{fgui_list.name} list screen.")
+            return list_code
+        
+        # 计算screen入参。
+        overflow = None
+        layout = 'row'
+        row_num = 1
+        column_num = 1
+        transpose = False
+        xspacing = 0
+        yspacing = 0
+        margin = (0, 0, 0, 0)
+        xysize = (0, 0)
+        pos = (0, 0)
+
+        # 根据列表布局计算行数与列数。
+        # layout-列表布局：(column)默认-单列竖排，row-单行横排，flow_hz-横向流动，flow_vt-纵向流动、pagination-分页
+        # lineItemCount：列表布局为横向流动或分页时，表示列数。列表布局为竖向流动时，表示行数。其他布局中，该参数无效果。
+        # lineItemCount2：列表布局为分页时，表示行数。其他布局中，该参数无效果。
+        if fgui_list.layout == "column":
+            column_num = 1
+            row_num = list_length
+        elif fgui_list.layout == "row":
+            row_num = 1
+            column_num = list_length
+        elif fgui_list.layout == 'flow_hz':
+            column_num = fgui_list.line_item_count
+            row_num = math.ceil(list_length / column_num)
+        elif fgui_list.layout == 'flow_vt':
+            row_num = fgui_list.line_item_count
+            column_num = math.ceil(list_length / row_num)
         else:
-            end_indent_level = 2
-            list_code.append(f"{self.indent_str}viewport:")
-            self.indent_level_up()
-            list_code.append(f"{self.indent_str}pos {fgui_list.xypos}")
-            list_code.append(f"{self.indent_str}xysize {fgui_list.size}")
-            if fgui_list.margin:
-                list_code.append(f"{self.indent_str}margin {fgui_list.margin}")
-
-            # 若“隐藏”，使用不可滚动的viewport
-            if fgui_list.overflow == "hidden":
-                list_code.append(f"{self.indent_str}draggable False")
-                list_code.append(f"{self.indent_str}mousewheel False")
-                # 单列竖排，使用vbox
-                if fgui_list.layout == "column":
-                    list_code.append(f"{self.indent_str}vbox:")
-                    self.indent_level_up()
-                    if fgui_list.line_gap:
-                        list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}")
-
-                # 单行横排，使用hbox
-                elif fgui_list.layout == "row":
-                    list_code.append(f"{self.indent_str}hbox:")
-                    self.indent_level_up()
-                    if fgui_list.line_gap:
-                        list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}")
-
-                # 其他，使用grid
-                else:
-                    # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
-                    if not (fgui_list.line_item_count and fgui_list.line_item_count2):
-                        # 横向流动，填充行，之后换行
-                        if fgui_list.layout == 'flow_hz' :
-                            column_num = math.floor(list_xysize[0] / default_item_size[0])
-                            row_num = math.ceil(list_length / column_num)
-                        # 纵向流动，先填充列，之后换列
-                        elif fgui_list.layout == 'flow_vt' :
-                            row_num = math.floor(list_xysize[1] / default_item_size[1])
-                            column_num = math.ceil(list_length / row_num)
-                        list_code.append(f"{self.indent_str}grid {column_num} {row_num}:")
-                    else:
-                        list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
-                    self.indent_level_up()
-                    if fgui_list.layout == 'flow_vt' :
-                        list_code.append(f"{self.indent_str}transpose True")
-                    if fgui_list.line_gap:
-                        list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
-                    if fgui_list.col_gap:
-                        list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
-
-            # 若“滚动”，使用可滚动的vpgrid。但RenPy无法限制某个轴能否滚动。
-            elif fgui_list.overflow == "scroll":
-                list_code.append(f"{self.indent_str}draggable True")
-                list_code.append(f"{self.indent_str}mousewheel True")
-                # 垂直滚动
-                if fgui_list.scroll == "vertical":
-                    pass
-                # 水平滚动
-                elif fgui_list.scroll == "horizontal":
-                    pass
-                # 自由滚动
-                elif fgui_list.scroll == "both":
-                    pass
-                # 单列竖排，使用vbox
-                if fgui_list.layout == "column":
-                    list_code.append(f"{self.indent_str}vbox:")
-                    self.indent_level_up()
-                    if fgui_list.line_gap:
-                        list_code.append(f"{self.indent_str}spacing {fgui_list.line_gap}")
-
-                # 单行横排，使用hbox
-                elif fgui_list.layout == "row":
-                    list_code.append(f"{self.indent_str}hbox:")
-                    self.indent_level_up()
-                    if fgui_list.line_gap:
-                        list_code.append(f"{self.indent_str}spacing {fgui_list.col_gap}")
-
-                # 其他，使用grid
-                else:
-                    # FguiList中的line_item_count和line_item_count2可能为0，需要根据列表尺寸与元素尺寸计算实际的行数与列数。
-                    if not (fgui_list.line_item_count and fgui_list.line_item_count2):
-                        # 横向流动，填充行，之后换行
-                        if fgui_list.layout == 'flow_hz' :
-                            column_num = math.floor(list_xysize[0] / default_item_size[0])
-                            row_num = math.ceil(list_length / column_num)
-                        # 纵向流动，先填充列，之后换列
-                        elif fgui_list.layout == 'flow_vt' :
-                            row_num = math.floor(list_xysize[1] / default_item_size[1])
-                            column_num = math.ceil(list_length / row_num)
-                        list_code.append(f"{self.indent_str}grid {column_num} {row_num}:")
-                    else:
-                        list_code.append(f"{self.indent_str}grid {fgui_list.line_item_count} {fgui_list.line_item_count2}:")
-                    self.indent_level_up()
-                    if fgui_list.layout == 'flow_vt' :
-                        list_code.append(f"{self.indent_str}transpose True")
-                    if fgui_list.line_gap:
-                        list_code.append(f"{self.indent_str}yspacing {fgui_list.line_gap}")
-                    if fgui_list.col_gap:
-                        list_code.append(f"{self.indent_str}xspacing {fgui_list.col_gap}")
-
+            row_num = fgui_list.line_item_count
+            column_num = fgui_list.line_item_count2
+
+        xspacing = fgui_list.col_gap
+        yspacing = fgui_list.line_gap
+        transpose = (fgui_list.layout == 'flow_vt')
+        xysize = fgui_list.size
+        pos = fgui_list.xypos
+        margin = fgui_list.margin
+        screen_params = f"overflow='{overflow}', layout='{layout}', row_num={row_num}, column_num={column_num}, transpose={transpose}, xspacing={xspacing}, yspacing={yspacing}, xysize={xysize}, pos={pos}, margin={margin}"
+        # 引用列表组件的screen。
+        list_code.append(f"{self.indent_str}use {component_name}_{fgui_list.name}({screen_params}):")
         # 添加元素
+        self.indent_level_up()
         for item in fgui_list.item_list:
             # 非默认元素
             if item.item_url:
@@ -2272,7 +2343,7 @@ def generate_list_displayable(self, fgui_list):
             # 默认元素
             else:
                 if default_item_type == "image":
-                    list_code.append(f"{self.indent_str}add \'{default_item_name}\'")
+                    list_code.append(f"{self.indent_str}add '{default_item_name}'")
                 elif default_item_type == "Button":
                     parameter_str = self.generate_button_parameter(item.item_title)
                     list_code.append(f"{self.indent_str}use {default_item_name}({parameter_str})")
@@ -2284,16 +2355,38 @@ def generate_list_displayable(self, fgui_list):
                     list_code.append(f"{self.indent_str}use {default_item_name}()")
                     self.indent_level_down()
 
-        self.indent_level_down(end_indent_level)
         return list_code
 
+    def generate_list_screen(self, fgui_list : FguiList, component_name : str) -> bool:
+        """
+        生成列表组件的screen。于其他screen中使用use语句引用。
+        """
+        if not isinstance(fgui_list, FguiList):
+            print("It is not a list displayable.")
+            return False
+
+        # 根据模板内容替换部分字符串
+        list_screen_template_content = self.list_screen_template
+        list_screen_template_content = list_screen_template_content.replace('{component_name}', f"{component_name}")
+        list_screen_template_content = list_screen_template_content.replace('{list_name}', f"{fgui_list.name}")
+        list_screen_template_content = list_screen_template_content.replace('{list_screen_name}', f"{component_name}_{fgui_list.name}")
+        vertical_scrollbar_component, horizontal_scrollbar_component = self.get_scrollbar_style(fgui_list.scrollbar_res)
+        vscrollbar_style = vertical_scrollbar_component.name if vertical_scrollbar_component else self.default_scrollbar_style
+        hscrollbar_style = horizontal_scrollbar_component.name if horizontal_scrollbar_component else self.default_scrollbar_style
+        list_screen_template_content = list_screen_template_content.replace('{vscrollbar_style}', f"'{vscrollbar_style}'")
+        list_screen_template_content = list_screen_template_content.replace('{hscrollbar_style}', f"'{hscrollbar_style}'")
+
+        self.screen_code.append(list_screen_template_content)
+
+        return True
+
     def generate_renpy_code(self):
         """生成完整的Ren'Py代码"""
 
 
         # 读取01_ui_helper.rpy文件
         self.gallery_data = self.get_gallery_data_from_ui_helper(os.path.join(self.game_dir, self.ui_helper_file_name))
-        print(f"gallery_data: {self.gallery_data}")
+        # print(f"gallery_data: {self.gallery_data}")
         self.renpy_code = []
 
         # 添加文件头注释
@@ -2311,7 +2404,7 @@ def generate_renpy_code(self):
             if component.extention == 'Button':
                 self.generate_button_screen(component)
             elif component.extention == 'ScrollBar':
-                pass
+                self.generate_scroll_bar_style(component)
             elif component.extention == 'Label':
                 pass
             elif component.extention == 'Slider':
@@ -2576,6 +2669,8 @@ def convert(argv):
         print("正在创建转换器...")
         converter = FguiToRenpyConverter(fgui_assets)
         converter.game_dir = game_dir
+        converter.scripts_dir = scripts_dir
+        converter.images_dir = images_dir
         print("转换器创建完成")
 
         # 生成Ren'Py代码
@@ -2584,16 +2679,11 @@ def convert(argv):
         print("Ren'Py代码生成完成")
 
         # 保存.rpy文件到game目录
-        # output_file = os.path.join(scripts_dir, "fgui_to_renpy.rpy")
-        global_variables_output_file = os.path.join(scripts_dir, "preppipe_global_variables.rpy")
-        image_definition_output_file = os.path.join(scripts_dir, "preppipe_image_definition.rpy")
-        style_output_file = os.path.join(scripts_dir, "preppipe_styles.rpy")
-        screen_output_file = os.path.join(scripts_dir, "preppipe_screens.rpy")
-        # converter.save_to_file(output_file)
-        # self.renpy_code.extend(self.game_global_variables_code)
-        # self.renpy_code.extend(self.image_definition_code)
-        # self.renpy_code.extend(self.graph_definition_code)
-        # self.renpy_code.extend(self.style_code)
+        global_variables_output_file = os.path.join(converter.scripts_dir, "preppipe_global_variables.rpy")
+        image_definition_output_file = os.path.join(converter.scripts_dir, "preppipe_image_definition.rpy")
+        style_output_file = os.path.join(converter.scripts_dir, "preppipe_styles.rpy")
+        screen_output_file = os.path.join(converter.scripts_dir, "preppipe_screens.rpy")
+
         converter.save_code_to_file(global_variables_output_file, converter.game_global_variables_code)
         converter.image_definition_code.extend(converter.graph_definition_code)
         converter.save_code_to_file(image_definition_output_file, converter.image_definition_code)
@@ -2601,16 +2691,16 @@ def convert(argv):
         converter.save_code_to_file(screen_output_file, converter.screen_code)
 
         # 部分预定义模板文件修改参数并保存
-        font_map_definition_file = os.path.join(scripts_dir, "01_font_map.rpy")
+        font_map_definition_file = os.path.join(converter.scripts_dir, "01_font_map.rpy")
         converter.from_templates_to_renpy(font_map_definition_file)
 
         # 复制预定义cdd和cds文件
-        converter.copy_predefine_files(converter.renpy_template_dir, scripts_dir)
+        converter.copy_predefine_files(converter.renpy_template_dir, converter.scripts_dir)
 
         # 复制图集文件到images目录
         print("\n正在复制图集文件...")
         current_dir = os.getcwd()
-        atlas_count = converter.copy_atlas_files(fgui_project_path, images_dir)
+        atlas_count = converter.copy_atlas_files(fgui_project_path, converter.images_dir)
         print(f"复制了 {atlas_count} 个图集文件")
 
         # 一些清理
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt
new file mode 100644
index 0000000..57eb36a
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt
@@ -0,0 +1,75 @@
+# {component_name} 内部列表 {list_name}
+screen {list_screen_name}(overflow=None, layout='row', row_num=1, column_num=1, transpose=False, xspacing=0, yspacing=0, xysize=(0,0), pos=(0,0), margin=(0,0,0,0)):
+    if overflow == 'visible':
+        frame:
+            margin margin
+            xysize xysize
+            pos pos
+            if layout == 'row':
+                hbox:
+                    spacing xspacing
+                    transclude
+            elif layout == 'column':
+                vbox:
+                    spacing yspacing
+                    transclude
+            else:
+                grid column_num row_num:
+                    transpose transpose
+                    xspacing xspacing
+                    yspacing yspacing
+                    transclude
+    elif overflow == 'hidden':
+        frame:
+            margin margin
+            has viewport
+            xysize xysize
+            pos pos
+            draggable False
+            mousewheel False
+            if layout == 'row':
+                hbox:
+                    spacing xspacing
+                    transclude
+            elif layout == 'column':
+                vbox:
+                    spacing yspacing
+                    transclude
+            else:
+                grid column_num row_num:
+                    transpose transpose
+                    xspacing xspacing
+                    yspacing yspacing
+                    transclude
+    else:
+        frame:
+            has side 'c r b'
+            viewport id '{list_screen_name}_vp':
+                draggable True
+                mousewheel True
+                pos pos
+                xysize xysize
+                if layout == 'row':
+                    hbox:
+                        spacing xspacing
+                        transclude
+                elif layout == 'column':
+                    vbox:
+                        spacing yspacing
+                        transclude
+                else:
+                    grid column_num row_num:
+                        transpose transpose
+                        xspacing xspacing
+                        yspacing yspacing
+                        transclude
+            vbar:
+                value YScrollValue('{list_screen_name}_vp')
+                style {vscrollbar_style}
+                ysize xysize[1]
+                pos pos
+            bar:
+                value XScrollValue('{list_screen_name}_vp')
+                style {hscrollbar_style}
+                xsize xysize[0]
+                pos pos
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_style_preset_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_style_preset_template.txt
new file mode 100644
index 0000000..b79154d
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_style_preset_template.txt
@@ -0,0 +1,3 @@
+style frame is empty:
+    padding (0,0,0,0)
+    background Null()

From cf7aa3d35af6a41db5b8e935cea0f5fbf520b8db Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 2 Feb 2026 14:25:14 +0800
Subject: [PATCH 21/40] Fix issue of bounding-box size with component.

---
 src/fgui_converter/FguiAssetsParseLib.py | 139 ++++++++++++++---------
 1 file changed, 83 insertions(+), 56 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index d614fb5..3a77ca5 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -10,7 +10,7 @@ class FguiPackage():
     FairyGUI资源包中的Package描述内容。
     包含component列表、image列表和atlas列表。
     """
-    desc_id = ''
+    id = ''
     name = ''
     component_list = []
     image_list = []
@@ -65,6 +65,7 @@ def __init__(self, package_etree):
         self.name = package_etree.get("name")
         self.resource = package_etree[0]
         self.id_name_mapping = {}
+        self.id_size_mapping = {}
         if (self.resource.tag != 'resources'):
             raise ValueError('packageDescription child is not resources.')
         for child in self.resource:
@@ -77,6 +78,7 @@ def __init__(self, package_etree):
                                                             child.get('size'),
                                                             exported=TransStrToBoolean(child.get('exported'))))
                 self.id_name_mapping[child.get('id')] = child.get('name')
+                self.id_size_mapping[child.get('id')] = tuple(map(int, child.get('size').split(",")))
             # image类 name, path, size, scale=None, scale9grid=[], exported=True
             if (child.tag == 'image'):
                 self.image_list.append(self.brief_image(
@@ -88,6 +90,7 @@ def __init__(self, package_etree):
                                                     child.get('scale9grid'),
                                                     exported=TransStrToBoolean(child.get('exported'))))
                 self.id_name_mapping[child.get('id')] = child.get('name')
+                self.id_size_mapping[child.get('id')] = tuple(map(int, child.get('size').split(",")))
             # atlas类 id, size, file
             if (child.tag == 'atlas'):
                 self.atlas_list.append(self.brief_atlas(
@@ -100,6 +103,19 @@ def clear(self):
         self.image_list.clear()
         self.atlas_list.clear()
         self.id_name_mapping.clear()
+        self.id_size_mapping.clear()
+
+    def get_image_by_id(self, image_id):
+        for image in self.image_list:
+            if image.id == image_id:
+                return image
+        return None
+
+    def get_component_by_id(self, component_id):
+        for component in self.component_list:
+            if component.id == component_id:
+                return component
+        return None
 
 def TransStrToBoolean(str):
     if (str == 'true' or str == 'True'):
@@ -254,11 +270,11 @@ class FguiComponent:
     id、name、path和size属性与package中brief_component的对应组件信息保持一致
     """
 
-    def __init__(self, component_etree, id, name, package_desc_id=None):
+    def __init__(self, component_etree, id, name, package_desc=None):
         self.component_etree = component_etree
         self.id = id
         self.name = name
-        self.package_desc_id = package_desc_id
+        self.package_desc = package_desc
         size = component_etree.get("size")
         self.size = tuple(map(int, size.split(","))) if size else (0,0)
         self.overflow = component_etree.get("overflow", "visible")
@@ -291,11 +307,15 @@ def __init__(self, component_etree, id, name, package_desc_id=None):
                 self.controller_list.append(FguiController(self.component_etree[i].get("name"), self.component_etree[i].get("pages"), self.component_etree[i].get("selected")))
             # 显示内容
             elif (self.component_etree[i].tag == "displayList"):
-                self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc_id)
+                self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc)
                 for displayable in self.display_list.displayable_list:
+                    # 此处的displayable组件的size可能省略(即原尺寸)，需要重新获取，不然值为(0,0)
+                    displayable_size = displayable.size
+                    if displayable_size == (0, 0):
+                        displayable.size = self.package_desc.id_size_mapping[displayable.src] if self.package_desc else (0, 0)
                     # 包围框尺寸。FGUI不考虑子组件坐标xy小于0的情况，仅扩展组件的右侧和下方。
-                    self.bbox_width = max(self.bbox_width, displayable.xypos[0] + displayable.size[0])
-                    self.bbox_height = max(self.bbox_height, displayable.xypos[1] + displayable.size[1])
+                    self.bbox_width = max(self.bbox_width, displayable.xypos[0] + displayable_size[0])
+                    self.bbox_height = max(self.bbox_height, displayable.xypos[1] + displayable_size[1])
 
     def __repr__(self):
         return f"FguiComponent({self.id}, {self.name}, {self.size}, {self.extention}, {self.mask})"
@@ -305,8 +325,8 @@ class FguiButton(FguiComponent):
     FairyGUI中的按钮button。
     相比其他component，多一个Button标签
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
         button = component_etree.find("Button")
         self.button_mode = button.get("mode")
         self.button_down_effect = button.get("downEffect")
@@ -318,8 +338,8 @@ class FguiScrollBar(FguiComponent):
     相比其他component，多一个ScrollBar标签，大部分情况为空。
     通常滚动条都会有以个对应的其他组件，为同名带后缀“_grip”的按钮。
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
         scrollbar = component_etree.find("ScrollBar")
 
 class FguiLabel(FguiComponent):
@@ -327,8 +347,8 @@ class FguiLabel(FguiComponent):
     FairyGUI中的标签lablel。
     目前与FguiComponent完全相同，甚至不具有单独的Label标签。
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
 
 
 class FguiComboBox(FguiComponent):
@@ -337,8 +357,8 @@ class FguiComboBox(FguiComponent):
     相比其他component，多一个ComboBox标签，属性dropdown对应点击后显示的选项列表。
     通常下拉框都会有两个对应的其他组件，分别为同名带后缀“_item”的按钮和同名带后缀“_popup”的组件
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
         combobox = component_etree.find("ComboBox")
         self.dropdown = combobox.get("dropdown")
 
@@ -347,8 +367,8 @@ class FguiProgressBar(FguiComponent):
     FairyGUI中的进度条。
     相比其他component，多一个ProgressBar标签，大部分情况为空。
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
         self.progressbar = component_etree.find("ProgressBar")
 
 class FguiSlider(FguiComponent):
@@ -357,8 +377,8 @@ class FguiSlider(FguiComponent):
     相比其他component，多一个Slider标签，大部分情况为空。
     滑动条会有一个相应的其他组件，同名带后缀“_grip”的按钮。
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
         self.slider = component_etree.find("Slider")
         self.title_type = self.slider.get("titleType")
 
@@ -377,8 +397,8 @@ class FguiWindow(FguiComponent):
     Ren'Py中没有FairyGUI的包体处理以上内容，且通常不需要动态创建组件。
     暂定按默认Component处理。可拖拽功能再议。
     """
-    def __init__(self, component_etree, id, name, package_desc_id=None):
-        super().__init__(component_etree, id, name, package_desc_id=None)
+    def __init__(self, component_etree, id, name, package_desc=None):
+        super().__init__(component_etree, id, name, package_desc=package_desc)
 
 
 class FguiDisplayList:
@@ -386,27 +406,26 @@ class FguiDisplayList:
     FairyGUI组件内部显示列表，xml中displayList标签内容。
     只要一个组件不为空组件，必定会有displayList。
     """
-    def __init__(self, display_list_etree, package_desc_id=None):
+    def __init__(self, display_list_etree, package_desc=None):
         self.display_list_etree = display_list_etree
-        self.package_desc_id = package_desc_id
-        # print(f"package_desc_id: {package_desc_id}")
+        self.package_desc = package_desc
         self.displayable_list = []
         for displayable in self.display_list_etree:
             # print(displayable.tag)
             # 根据标签类型创建相应的FguiDisplayable对象
             if displayable.tag == "graph":
-                self.displayable_list.append(FguiGraph(displayable))
+                self.displayable_list.append(FguiGraph(displayable, self.package_desc))
             elif displayable.tag == "text":
-                self.displayable_list.append(FguiText(displayable))
+                self.displayable_list.append(FguiText(displayable, self.package_desc))
             elif displayable.tag == "image":
-                self.displayable_list.append(FguiImage(displayable))
+                self.displayable_list.append(FguiImage(displayable, self.package_desc))
             elif displayable.tag == "list":
-                self.displayable_list.append(FguiList(displayable, self.package_desc_id))
+                self.displayable_list.append(FguiList(displayable, self.package_desc))
             elif displayable.tag == "loader":
-                self.displayable_list.append(FguiLoader(displayable))
+                self.displayable_list.append(FguiLoader(displayable, self.package_desc))
             else:
                 # 对于未知类型，创建基础的FguiDisplayable对象
-                self.displayable_list.append(FguiDisplayable(displayable))
+                self.displayable_list.append(FguiDisplayable(displayable, self.package_desc))
 
 def hex_aarrggbb_to_rgba(hex_color):
     """
@@ -474,8 +493,9 @@ class FguiDisplayable:
     gearText-根据控制器决定文本组件显示的文本内容；
     gearIcon-根据控制器决定装载器显示内容。
     """
-    def __init__(self, display_item_tree, package_description_id=None):
+    def __init__(self, display_item_tree, package_desc=None):
         self.display_item_tree = display_item_tree
+        self.package_desc = package_desc
         # id
         self.id = self.display_item_tree.get("id")
         # 名称
@@ -487,7 +507,10 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.xypos = tuple(map(int, xy.split(",")))
         # 尺寸。若为None：image的size默认与image对象一致。
         size = self.display_item_tree.get("size")
-        self.size = tuple(map(int, size.split(","))) if size else (0,0)
+        if not size:
+            self.size = self.package_desc.id_size_mapping[self.src] if self.package_desc else (0, 0)
+        else:
+            self.size = tuple(map(int, size.split(","))) if size else (0,0)
         # 保持比例。若为None，则将宽和高分别缩放到size。
         self.aspect = (self.display_item_tree.get("aspect") == "true")
         # 缩放。默认值为(1.0, 1.0)。
@@ -514,7 +537,8 @@ def __init__(self, display_item_tree, package_description_id=None):
         # 未明确是否可触摸则默认可以触摸
         self.touchable = not (self.display_item_tree.get("touchable") == "false")
         # 资源包描述id，部分组件需要
-        self.package_description_id = package_description_id
+        self.package_desc = package_desc
+        self.package_description_id = package_desc.id if package_desc else None
         # BlendMode
         self.blend_mode = self.display_item_tree.get("blend", "normal")
         # 滤镜
@@ -610,10 +634,10 @@ class FguiGraph(FguiDisplayable):
     """
     FairyGUI中的图形。包括空白、矩形(圆边矩形)、圆形(椭圆)、多边形等。
     """
-    def __init__(self, display_item_tree):
+    def __init__(self, display_item_tree, package_desc=None):
         if display_item_tree.tag != "graph" :
             raise ValueError("xml tag is not graph.")
-        super().__init__(display_item_tree)
+        super().__init__(display_item_tree, package_desc)
         # None: 空白  rect: 矩形(可带圆角) eclipse: 椭圆(包括圆形)  regular_polygon: 正多边形  polygon: 多边形
         self.type = self.display_item_tree.get("type", None)
         self.stroke_width = int(self.display_item_tree.get("lineSize", "1"))
@@ -646,10 +670,10 @@ class FguiText(FguiDisplayable):
     """
     FairyGUI中的文本。
     """
-    def __init__(self, display_item_tree):
+    def __init__(self, display_item_tree, package_desc=None):
         if display_item_tree.tag != "text" :
             raise ValueError("xml tag is not text.")
-        super().__init__(display_item_tree)
+        super().__init__(display_item_tree, package_desc)
         # FairyGUI编辑器未设置全局字体的情况下，默认渲染使用Arial，与Ren'Py默认字体SourceHanSansLite不同。
         # 此处的字体与FairyGUI编辑器中的字体属性不同，为字体名称。编辑器中的xml则记录引用项。
         # 另外，FairyGUI发布的资源文件中并不包含字体文件，需要手工放置到游戏引擎对应目录。
@@ -698,15 +722,17 @@ class FguiImage(FguiDisplayable):
     样例：
     <image id="n9_z1bv" name="n9" src="ndicp" xy="210,169" alpha="0.67" color="#000000" flip="both" fillMethod="hz" fillOrigin="1" fillAmount="55"/>
     """
-    def __init__(self, display_item_tree):
+    def __init__(self, display_item_tree, package_desc=None):
         if display_item_tree.tag != "image" :
             raise ValueError("xml tag is not image.")
-        super().__init__(display_item_tree)
+        super().__init__(display_item_tree, package_desc)
         self.multiply_color = self.display_item_tree.get("color", "#ffffff")
         self.flip_type = self.display_item_tree.get("flip")
         self.fill_method = self.display_item_tree.get("fillMethod")
         self.fill_origin = self.display_item_tree.get("fillOrigin")
         self.fill_amount = self.display_item_tree.get("fillAmount")
+        if self.size == (0, 0):
+            self.size = self.package_desc.get_image_by_id(self.src).size if self.package_desc else (0, 0)
 
 class FguiListItem():
     """
@@ -763,7 +789,7 @@ class FguiList(FguiDisplayable):
     colGap：列距。
     defaultItem：默认元素，通常是一个资源url，格式为“ui://”+“packageDescription id” + “component id”。
     """
-    def __init__(self, display_item_tree, package_description_id=None):
+    def __init__(self, display_item_tree, package_desc=None):
         if display_item_tree.tag != "list" :
             raise ValueError("xml tag is not list.")
         super().__init__(display_item_tree)
@@ -771,7 +797,7 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.overflow = self.display_item_tree.get("overflow", "visible")
         self.scroll = self.display_item_tree.get("scroll", "vertical")
         scrollbar_res = self.display_item_tree.get("scrollBarRes")
-        self.scrollbar_res = self.get_scrollbar_res(scrollbar_res, package_description_id)
+        self.scrollbar_res = self.get_scrollbar_res(scrollbar_res, package_desc.id)
         self.scroll_bar_flags = int(self.display_item_tree.get("scrollBarFlags", "256"))
         margin = self.display_item_tree.get("margin")
         self.margin = tuple(map(int, margin.split(","))) if margin else (0,0,0,0)
@@ -782,9 +808,9 @@ def __init__(self, display_item_tree, package_description_id=None):
         self.col_gap = int(self.display_item_tree.get("colGap", "0"))
         self.default_item_url = self.display_item_tree.get("defaultItem")
         self.default_item_id = None
-        self.package_description_id = package_description_id
-        if package_description_id:
-            self.get_default_item(package_description_id)
+        self.package_description_id = package_desc.id
+        if package_desc:
+            self.get_default_item(package_desc.id)
         self.item_list = []
         for item_tree in display_item_tree:
             item = FguiListItem(item_tree, self.package_description_id)
@@ -819,15 +845,15 @@ class FguiLoader(FguiDisplayable):
     FairyGUI中的装载器。
     包含属性url：表示引用的组件url，通常是一个资源url，格式为“ui://”+“packageDescription id” + “component id”。
     """
-    def __init__(self, display_item_tree, package_description_id=None):
+    def __init__(self, display_item_tree, package_desc=None):
         if display_item_tree.tag != "loader" :
             raise ValueError("xml tag is not loader.")
-        super().__init__(display_item_tree)
+        super().__init__(display_item_tree, package_desc)
         self.url = self.display_item_tree.get("url")
         self.item_url = None
-        if package_description_id:
-            self.package_description_id = package_description_id
-            self.get_item_id(package_description_id)
+        if package_desc and self.url:
+            self.package_description_id = package_desc.id
+            self.get_item_id(package_desc.id)
 
     def get_item_id(self, packageDescription_id):
         self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):]
@@ -1084,15 +1110,15 @@ def __init__(self, fgui_project_path):
             extention_type = self.object_dict[component.id].get("extention")
             # 根据extention构造不同对象
             if extention_type == "Button":
-                component = FguiButton(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+                component = FguiButton(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             elif extention_type == "ScrollBar":
-                component = FguiScrollBar(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+                component = FguiScrollBar(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             elif extention_type == "Label":
-                component = FguiLabel(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+                component = FguiLabel(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             elif extention_type == "Slider":
-                component = FguiSlider(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+                component = FguiSlider(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             else:
-                component = FguiComponent(self.object_dict[component.id], component.id, component.name, package_desc_id=self.package_desc.id)
+                component = FguiComponent(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             self.fgui_component_set.append(component)
         # 根据atlas_list建立altas_id与实际图集文件间的映射关系
         for atlas in self.package_desc.atlas_list:
@@ -1122,9 +1148,10 @@ def get_component_by_id(self, id : str) -> FguiComponent:
         return None
 
     def get_image_size_by_id(self, id : str):
-        for image in self.fgui_image_set:
-            if image.image_id == id:
-                return (image.width, image.height)
+        # for image in self.fgui_image_set:
+        #     if image.image_id == id:
+        #         return (image.width, image.height)
+        return self.package_desc.id_size_mapping[id]
 
     def __del__(self):
         self.clear()

From ac0375f79f4ec1eae6c7a1db26bb248a52d85835 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Thu, 12 Feb 2026 13:57:02 +0800
Subject: [PATCH 22/40] Add ScriptFlowEditor module

1. Fgui assets filter initial value fixed to (0,0,0,0).
2. Add script flow editor visualization module. The first version of data model.
---
 .gitignore                                    |   3 +
 src/ScriptFlowEditor/__init__.py              |  25 +++
 .../basicflow_deserialization_test.py         |  16 ++
 .../basicflow_serialization_test.py           |  37 ++++
 src/ScriptFlowEditor/models/__init__.py       |  17 ++
 src/ScriptFlowEditor/models/flag.py           |  88 +++++++++
 src/ScriptFlowEditor/models/gamescriptflow.py | 187 ++++++++++++++++++
 src/ScriptFlowEditor/models/path.py           |  39 ++++
 src/ScriptFlowEditor/models/segment.py        |  40 ++++
 src/fgui_converter/FguiAssetsParseLib.py      |   9 +-
 .../utils/renpy/Fgui2RenpyConverter.py        |  16 +-
 11 files changed, 465 insertions(+), 12 deletions(-)
 create mode 100644 src/ScriptFlowEditor/__init__.py
 create mode 100644 src/ScriptFlowEditor/basicflow_deserialization_test.py
 create mode 100644 src/ScriptFlowEditor/basicflow_serialization_test.py
 create mode 100644 src/ScriptFlowEditor/models/__init__.py
 create mode 100644 src/ScriptFlowEditor/models/flag.py
 create mode 100644 src/ScriptFlowEditor/models/gamescriptflow.py
 create mode 100644 src/ScriptFlowEditor/models/path.py
 create mode 100644 src/ScriptFlowEditor/models/segment.py

diff --git a/.gitignore b/.gitignore
index 9f59a9d..799474f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,6 @@ versionfile_*.txt
 
 run_cli.py
 run_gui.py
+
+# ScriptFlowEditor generated files
+src/ScriptFlowEditor/generated
\ No newline at end of file
diff --git a/src/ScriptFlowEditor/__init__.py b/src/ScriptFlowEditor/__init__.py
new file mode 100644
index 0000000..460ddf3
--- /dev/null
+++ b/src/ScriptFlowEditor/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+"""
+文字 ADV 脚本流程编辑器的数据模型与（后续）视图层入口。
+
+当前仅实现数据模型层：
+- 剧情段落（节点）
+- 段落间连接条件
+- 段落连接条件相关变量（flag）
+"""
+
+from ScriptFlowEditor.models import (
+    FlagType,
+    FlagVariable,
+    StorySegment,
+    SegmentPath,
+    GameScriptFlow,
+)
+
+__all__ = [
+    "FlagType",
+    "FlagVariable",
+    "StorySegment",
+    "SegmentPath",
+    "GameScriptFlow",
+]
diff --git a/src/ScriptFlowEditor/basicflow_deserialization_test.py b/src/ScriptFlowEditor/basicflow_deserialization_test.py
new file mode 100644
index 0000000..442b758
--- /dev/null
+++ b/src/ScriptFlowEditor/basicflow_deserialization_test.py
@@ -0,0 +1,16 @@
+from pathlib import Path
+from ScriptFlowEditor import GameScriptFlow
+
+# 项目根目录（本文件在 src/ScriptFlowEditor/basicflow_deserialization_test.py）
+base = Path(__file__).resolve().parent.parent.parent
+path = base / "src" / "ScriptFlowEditor" / "generated" / "Demo Flow.json"
+flow = GameScriptFlow.load_from_json(path)
+
+print("已加载流程")
+print(f"名称: {flow.name}")
+print(f"id: {flow.id}")
+print(f"标题: {flow.title}")
+print(f"注释: {flow.comment}")
+print(f"各段落: {flow.segments}")
+print(f"各路径: {flow.paths}")
+print(f"flag变量: {flow.flags}")
\ No newline at end of file
diff --git a/src/ScriptFlowEditor/basicflow_serialization_test.py b/src/ScriptFlowEditor/basicflow_serialization_test.py
new file mode 100644
index 0000000..d9ae53d
--- /dev/null
+++ b/src/ScriptFlowEditor/basicflow_serialization_test.py
@@ -0,0 +1,37 @@
+# 基础流程及序列化保存测试
+
+from ScriptFlowEditor import *
+
+# flag变量
+first_choice = FlagVariable(name='choice_1', flag_type=FlagType.BOOL, initial_value=True, comment='The 1st choice flag.')
+second_choice = FlagVariable(name='choice_2', flag_type=FlagType.BOOL, initial_value=False, comment='The 2nd choice flag.')
+
+# start，整个流程入口
+start_node = StorySegment(name='start', content='This is start label.', is_ending_segment=False, id=None)
+
+# 分支段落1
+branch_seg_1 = StorySegment(name='branch_seg_1', content='This is 1st branch segment label.', is_ending_segment=False, id=None)
+# 分支段落2
+branch_seg_2 = StorySegment(name='branch_seg_2', content='This is 2nd branch segment label.', is_ending_segment=False, id=None)
+
+# 结局1。从branch_seg_1跳转到该结局。
+ending_1 = StorySegment(name='ending_1', content='This is ending 1.', is_ending_segment=True)
+# 结局2。在second_choice为True时，从branch_seg_2跳转到该结局。
+ending_2 = StorySegment(name='ending_2', content='This is ending 2.', is_ending_segment=True)
+# 结局3。在first_choice为False时，从branch_seg_2跳转到该结局。
+ending_3 = StorySegment(name='ending_3', content='This is ending 3.', is_ending_segment=True)
+
+# 路径1。在start_node结尾，若first_choice为True则进入branch_seg_1。
+path_1 = SegmentPath(prev_segment_id=start_node.id, next_segment_id=branch_seg_1.id, condition_expression="choice_1 == True")
+# 路径2。在start_node结尾，若first_choice为False则进入branch_seg_2。
+path_2 = SegmentPath(prev_segment_id=start_node.id, next_segment_id=branch_seg_2.id, condition_expression="choice_1 == False")
+# 路径3。在branch_seg_1结尾，无条件进入ending_1。
+path_3 = SegmentPath(prev_segment_id=branch_seg_1.id, next_segment_id=ending_1.id, condition_expression=None)
+# 路径4。在branch_seg_2结尾，若second_choice为True则进入ending_2。
+path_4 = SegmentPath(prev_segment_id=branch_seg_2.id, next_segment_id=ending_2.id, condition_expression="choice_2 == True")
+# 路径5。在branch_seg_2结尾，若second_choice为False则进入ending_3。
+path_5 = SegmentPath(prev_segment_id=branch_seg_2.id, next_segment_id=ending_3.id, condition_expression="choice_2 == False")
+
+flow = GameScriptFlow(name="Demo Flow", segments=[start_node, branch_seg_1, branch_seg_2, ending_1, ending_2, ending_3], paths=[path_1, path_2, path_3, path_4, path_5], flags=[first_choice, second_choice])
+
+flow.save_as_json()
diff --git a/src/ScriptFlowEditor/models/__init__.py b/src/ScriptFlowEditor/models/__init__.py
new file mode 100644
index 0000000..9de18ac
--- /dev/null
+++ b/src/ScriptFlowEditor/models/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+"""脚本流程编辑器数据模型。"""
+
+from ScriptFlowEditor.models.flag import FlagType
+from ScriptFlowEditor.models.flag import FlagVariable
+from ScriptFlowEditor.models.segment import StorySegment
+from ScriptFlowEditor.models.path import SegmentPath
+from ScriptFlowEditor.models.gamescriptflow import GameScriptFlow
+
+
+__all__ = [
+    "FlagType",
+    "FlagVariable",
+    "StorySegment",
+    "SegmentPath",
+    "GameScriptFlow",
+]
diff --git a/src/ScriptFlowEditor/models/flag.py b/src/ScriptFlowEditor/models/flag.py
new file mode 100644
index 0000000..b4942de
--- /dev/null
+++ b/src/ScriptFlowEditor/models/flag.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+"""段落连接条件相关变量(flag)模型。"""
+
+from __future__ import annotations
+
+import hashlib
+from dataclasses import dataclass, field
+
+import enum
+
+
+class FlagType(enum.Enum):
+
+    """
+    flag变量类型。
+    
+    bool: 布尔值
+    int: 整数
+    float: 浮点数
+    """
+    BOOL = "bool"
+    INT = "int"
+    FLOAT = "float"
+
+
+@dataclass
+class FlagVariable:
+    """
+    控制分支走向相关变量(flag）。
+    用于在段落间连接条件中引用，具备类型与初始值。
+    """
+
+    # 变量名称，全局范围内应唯一。
+    name: str
+
+    # 变量类型：bool / int / float。入参可为 str（如 "bool"）或 FlagType，字符串会转换为枚举。
+    flag_type: str | FlagType
+
+    # 初始值，类型需与 flag_type 一致。
+    initial_value: bool | int | float
+
+    # 注释。
+    comment: str | None = None
+
+    # 唯一标识，由 name 哈希生成 8 位；不传则自动生成。
+    id: str | None = None
+
+    def __post_init__(self) -> None:
+        if self.id is None:
+            self.id = hashlib.sha256(self.name.encode()).hexdigest()[:8]
+        self._normalize_flag_type()
+        self._validate_initial_value()
+
+    # 将入参转换为 FlagType，无效则抛错。
+    def _normalize_flag_type(self) -> None:
+        if isinstance(self.flag_type, str):
+            try:
+                self.flag_type = FlagType(self.flag_type)
+            except ValueError:
+                valid = [e.value for e in FlagType]
+                raise ValueError(
+                    f"FlagVariable {self.name!r}: flag_type value is invalid {self.flag_type!r}, "
+                    f"it should be one of {valid}"
+                ) from None
+        elif not isinstance(self.flag_type, FlagType):
+            raise TypeError(
+                f"FlagVariable {self.name!r}: flag_type should be str or FlagType, "
+                f"got {type(self.flag_type).__name__}"
+            )
+
+    # 校验初始值类型是否与 flag_type 一致。
+    def _validate_initial_value(self) -> None:
+        
+        if self.flag_type == FlagType.BOOL and not isinstance(self.initial_value, bool):
+            raise TypeError(
+                f"FlagVariable {self.name!r}: flag_type is bool, "
+                f"initial_value should be bool, got {type(self.initial_value).__name__}"
+            )
+        if self.flag_type == FlagType.INT and not isinstance(self.initial_value, int):
+            raise TypeError(
+                f"FlagVariable {self.name!r}: flag_type is int, "
+                f"initial_value should be int, got {type(self.initial_value).__name__}"
+            )
+        if self.flag_type == FlagType.FLOAT and not isinstance(self.initial_value, (int, float)):
+            raise TypeError(
+                f"FlagVariable {self.name!r}: flag_type is float, "
+                f"initial_value should be int or float, got {type(self.initial_value).__name__}"
+            )
diff --git a/src/ScriptFlowEditor/models/gamescriptflow.py b/src/ScriptFlowEditor/models/gamescriptflow.py
new file mode 100644
index 0000000..89eafd0
--- /dev/null
+++ b/src/ScriptFlowEditor/models/gamescriptflow.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+"""脚本文档聚合模型：段落、连接、变量。"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+from dataclasses import asdict, dataclass, field
+from ScriptFlowEditor.models.flag import FlagType, FlagVariable
+from ScriptFlowEditor.models.segment import StorySegment
+from ScriptFlowEditor.models.path import SegmentPath
+
+
+@dataclass
+class GameScriptFlow:
+    """
+    游戏脚本流程。
+
+    聚合所有剧情段落、段落间分支路径、以及分支路径条件相关变量(flag)，
+    构成一份完整的游戏脚本流程数据。
+    """
+    # 脚本名称，用于在编辑器中显示与引用。
+    name: str = ""
+
+    # 剧情段落（节点）列表。
+    segments: list[StorySegment] = field(default_factory=list)
+
+    # 段落间分支路径列表。
+    paths: list[SegmentPath] = field(default_factory=list)
+
+    # 分支路径条件相关变量列表。
+    flags: list[FlagVariable] = field(default_factory=list)
+
+    # 脚本流程标题（可选，用于显示）。
+    title: str = ""
+
+    # 注释。
+    comment: str | None = None
+
+    # 唯一标识，由 name 哈希生成 8 位；不传则自动生成。
+    id: str | None = None
+
+    def __post_init__(self) -> None:
+        if self.id is None:
+            self.id = hashlib.sha256(self.name.encode()).hexdigest()[:8]
+
+    # 按 id 查找剧情段落。
+    def get_segment_by_id(self, segment_id: str) -> StorySegment | None:
+        for s in self.segments:
+            if s.id == segment_id:
+                return s
+        return None
+
+    # 按 id 查找变量。
+    def get_flag_by_id(self, flag_id: str) -> FlagVariable | None:
+        for f in self.flags:
+            if f.id == flag_id:
+                return f
+        return None
+
+    # 按名称查找变量。
+    def get_flag_by_name(self, name: str) -> FlagVariable | None:
+        for f in self.flags:
+            if f.name == name:
+                return f
+        return None
+
+    # 按前置段落 id 查找分支路径。
+    def get_path_prev(self, prev_segment_id: str) -> list[SegmentPath]:
+        return [c for c in self.paths if c.prev_segment_id == prev_segment_id]
+
+    # 按后续段落 id 查找分支路径。
+    def get_path_next(self, next_segment_id: str) -> list[SegmentPath]:
+        return [c for c in self.paths if c.next_segment_id == next_segment_id]
+
+    # 序列化为可 JSON 序列化的字典（JSON 对象）。
+    def to_dict(self) -> dict:
+        return {
+            "name": self.name,
+            "id": self.id,
+            "title": self.title,
+            "comment": self.comment,
+            "segments": [asdict(s) for s in self.segments],
+            "paths": [asdict(p) for p in self.paths],
+            "flags": [self._flag_to_dict(f) for f in self.flags],
+        }
+
+    # 将 FlagVariable 转为 dict，flag_type 枚举转为字符串。
+    @staticmethod
+    def _flag_to_dict(f: FlagVariable) -> dict:
+        d = asdict(f)
+        if hasattr(f.flag_type, "value"):
+            d["flag_type"] = f.flag_type.value
+        return d
+
+    # 序列化为 JSON 字符串。
+    def to_json(self, *, indent: int | None = None, ensure_ascii: bool = False) -> str:
+        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=ensure_ascii)
+
+    def _get_generated_dir(self) -> Path:
+        """返回 ScriptFlowEditor/generated 目录路径，不存在则创建。"""
+        # 本文件位于 ScriptFlowEditor/models/gamescriptflow.py
+        generated = Path(__file__).resolve().parent.parent / "generated"
+        generated.mkdir(parents=True, exist_ok=True)
+        return generated
+
+    @classmethod
+    def load_from_json(cls, path: str | Path) -> GameScriptFlow:
+        """
+        从 JSON 文件反序列化为 GameScriptFlow 对象。
+
+        :param path: JSON 文件路径，例如 src/ScriptFlowEditor/generated/Demo Flow.json
+        :return: 反序列化得到的 GameScriptFlow 实例
+        """
+        path = Path(path)
+        data = json.loads(path.read_text(encoding="utf-8"))
+        segments = [cls._segment_from_dict(d) for d in data.get("segments", [])]
+        paths = [cls._path_from_dict(d) for d in data.get("paths", [])]
+        flags = [cls._flag_from_dict(d) for d in data.get("flags", [])]
+        return cls(
+            name=data.get("name", ""),
+            id=data.get("id"),
+            title=data.get("title", ""),
+            comment=data.get("comment"),
+            segments=segments,
+            paths=paths,
+            flags=flags,
+        )
+
+    @staticmethod
+    def _segment_from_dict(d: dict) -> StorySegment:
+        return StorySegment(
+            name=d["name"],
+            content=d["content"],
+            is_ending_segment=d.get("is_ending_segment", True),
+            id=d.get("id"),
+            comment=d.get("comment", ""),
+            paths_segment_ids=d.get("paths_segment_ids", {}),
+        )
+
+    @staticmethod
+    def _path_from_dict(d: dict) -> SegmentPath:
+        return SegmentPath(
+            prev_segment_id=d["prev_segment_id"],
+            next_segment_id=d["next_segment_id"],
+            condition_expression=d.get("condition_expression"),
+            name=d.get("name"),
+            comment=d.get("comment"),
+            id=d.get("id"),
+        )
+
+    @staticmethod
+    def _flag_from_dict(d: dict) -> FlagVariable:
+        return FlagVariable(
+            name=d["name"],
+            flag_type=d["flag_type"],
+            initial_value=d["initial_value"],
+            comment=d.get("comment"),
+            id=d.get("id"),
+        )
+
+    # 序列化为 JSON 并保存到 ScriptFlowEditor/generated 目录下的 .json 文件。
+    def save_as_json(
+        self,
+        filename: str | None = None,
+        *,
+        indent: int | None = 2,
+        ensure_ascii: bool = False,
+    ) -> Path:
+        """
+        将当前对象序列化为 JSON 并保存到 src/ScriptFlowEditor/generated 目录。
+
+        :param filename: 文件名（可含 .json 后缀）；为 None 时用 name 或 id，不含则自动加 .json。
+        :param indent: 传给 json.dumps 的缩进，默认 2。
+        :param ensure_ascii: 是否转义非 ASCII，默认 False 以保留中文。
+        :return: 保存后的文件路径。
+        """
+        out_dir = self._get_generated_dir()
+        if not filename:
+            base = (self.name or self.id or "gamescriptflow").replace("/", "_").replace("\\", "_").strip(". ") or "flow"
+            filename = f"{base}.json" if not base.lower().endswith(".json") else base
+        elif not filename.lower().endswith(".json"):
+            filename = f"{filename}.json"
+        path = out_dir / filename
+        path.write_text(self.to_json(indent=indent, ensure_ascii=ensure_ascii), encoding="utf-8")
+        return path
diff --git a/src/ScriptFlowEditor/models/path.py b/src/ScriptFlowEditor/models/path.py
new file mode 100644
index 0000000..b5e9d05
--- /dev/null
+++ b/src/ScriptFlowEditor/models/path.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+"""段落间连接路径模型。"""
+
+from __future__ import annotations
+
+import hashlib
+from dataclasses import dataclass, field
+
+@dataclass
+class SegmentPath:
+    """
+    段落间分支路径。
+    满足条件表达式时，可以通往目标段落。
+    """
+
+    # 前置剧情段落id。
+    prev_segment_id: str
+
+    # 后续剧情段落id。
+    next_segment_id: str
+
+    # 条件表达式或模式字符串，为空表示无条件。
+    condition_expression: str|None = None
+
+    # 路径名称，用于在编辑器中显示与引用。为空时按源与目标段落id拼接。
+    name: str | None = None
+
+    # 注释。
+    comment: str | None = None
+
+    # 唯一标识，由 name 哈希生成 8 位；不传则自动生成。
+    id: str | None = None
+
+    def __post_init__(self) -> None:
+        if self.name is None or self.name == "":
+            self.name = f"{self.prev_segment_id}_to_{self.next_segment_id}_path"
+        if self.id is None or self.id == "":
+            self.id = hashlib.sha256(self.name.encode()).hexdigest()[:8]
+
diff --git a/src/ScriptFlowEditor/models/segment.py b/src/ScriptFlowEditor/models/segment.py
new file mode 100644
index 0000000..abca548
--- /dev/null
+++ b/src/ScriptFlowEditor/models/segment.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+"""剧情段落(节点)模型。"""
+
+from __future__ import annotations
+
+import hashlib
+from dataclasses import dataclass, field
+
+
+@dataclass
+class StorySegment:
+    """
+    剧情段落(节点)。
+    表示一段可播放的剧情内容，具备名称、正文以及是否为终点段落的属性。
+    """
+
+    # 段落名称，用于在编辑器中显示。
+    name: str
+
+    # 段落正文内容(脚本文本)。
+    content: str
+
+    # 是否为终点段落；默认为 True。如果为 False，则有后继连接。 如果为 True，则没有后继连接。
+    is_ending_segment: bool = True
+
+    # 唯一标识，由 name 哈希生成 8 位；不传则自动生成。
+    id: str | None = None
+
+    # 注释。
+    comment: str = ""
+
+    # 分支路径id与后续段落id字典。
+    paths_segment_ids: dict[str, str] = field(default_factory=dict)
+
+    def __post_init__(self) -> None:
+        if self.id is None:
+            self.id = hashlib.sha256(self.name.encode()).hexdigest()[:8]
+
+    def add_path_segment_id(self, path_id: str, segment_id: str) -> None:
+        self.paths_segment_ids[path_id] = segment_id
diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 3a77ca5..248d12a 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -541,10 +541,11 @@ def __init__(self, display_item_tree, package_desc=None):
         self.package_description_id = package_desc.id if package_desc else None
         # BlendMode
         self.blend_mode = self.display_item_tree.get("blend", "normal")
-        # 滤镜
-        self.color_filter = self.display_item_tree.get("filter")
-        # 滤镜颜色变换
-        self.color_filter_values = ColorFilterData(self.display_item_tree.get("filterData", "1,1,1,1"))
+        # 滤镜类型，一般是color，表示颜色滤镜
+        self.filter_type = self.display_item_tree.get("filter")
+        # 滤镜的值
+        if self.filter_type == "color":
+            self.color_filter_values = ColorFilterData(self.display_item_tree.get("filterData", "0,0,0,0"))
         # Tooltips，一般指指针悬垂在组件上时显示的说明文本。
         self.tooltips = self.display_item_tree.get("tooltips")
         # 自定义数据
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 0ea94e0..8be067f 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -217,13 +217,13 @@ def generate_image_definitions(self):
 
     def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : str) -> list:
         """
-        生成图形组件定义。返回字符串用于非screen定义。
-        图形组件有多种类别：
+        由FairyGUI的图形控件生成图形组件定义。返回字符串用于非screen定义。
+        FairyGUI的图形控件有多种类别：
         None: 空白
         rect: 矩形(可带圆角)
         eclipse: 椭圆(包括圆形)
-        regular_polygon: 正多边形
-        polygon: 多边形
+        regular_polygon: 正多边形，暂不支持
+        polygon: 多边形，暂不支持
         """
         graph_code = []
         graph_img_def = ''
@@ -557,10 +557,10 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
             # 图片组件
             if isinstance(displayable, FguiImage):
                 screen_ui_code.extend(self.generate_image_displayable(displayable))
-            # 图形组件
+            # 图形控件
             elif isinstance(displayable, FguiGraph):
                 screen_ui_code.extend(self.generate_graph_displayable(displayable))
-            # 文本组件
+            # 文本控件
             elif isinstance(displayable, FguiText):
                 screen_ui_code.extend(self.generate_text_displayable(displayable))
             # 列表
@@ -2071,8 +2071,8 @@ def generate_image_displayable(self, fgui_image : FguiImage) -> list:
 
     def generate_graph_displayable(self, fgui_graph : FguiGraph) -> list:
         """
-        生成图形组件。用于screen中。
-        图形组件有多种类别：
+        由FairyGUI的图形控件生成图形组件。用于screen中。
+        FairyGUI的图形控件有多种类别：
         None: 空白
         rect: 矩形(可带圆角)
         eclipse: 椭圆(包括圆形)

From 4c8f8fe05430ea468b2726c1423b8b184ef9f09f Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sat, 14 Feb 2026 19:06:18 +0800
Subject: [PATCH 23/40] Integrated Script Flow Editor with main pipe.

---
 src/ScriptFlowEditor/models/gamescriptflow.py |  10 +
 src/preppipe_gui_pyside6/mainwindow.py        |   5 +
 .../mainwindowinterface.py                    |   5 +
 src/preppipe_gui_pyside6/toolwidgets/home.py  |   2 +
 .../toolwidgets/scriptfloweditor.py           | 953 ++++++++++++++++++
 5 files changed, 975 insertions(+)
 create mode 100644 src/preppipe_gui_pyside6/toolwidgets/scriptfloweditor.py

diff --git a/src/ScriptFlowEditor/models/gamescriptflow.py b/src/ScriptFlowEditor/models/gamescriptflow.py
index 89eafd0..dca4e37 100644
--- a/src/ScriptFlowEditor/models/gamescriptflow.py
+++ b/src/ScriptFlowEditor/models/gamescriptflow.py
@@ -115,6 +115,16 @@ def load_from_json(cls, path: str | Path) -> GameScriptFlow:
         """
         path = Path(path)
         data = json.loads(path.read_text(encoding="utf-8"))
+        return cls.load_from_dict(data)
+
+    @classmethod
+    def load_from_dict(cls, data: dict) -> GameScriptFlow:
+        """
+        从字典反序列化为 GameScriptFlow 对象（可含额外键如 node_positions，会被忽略）。
+
+        :param data: 含 segments、paths、flags 等键的字典
+        :return: 反序列化得到的 GameScriptFlow 实例
+        """
         segments = [cls._segment_from_dict(d) for d in data.get("segments", [])]
         paths = [cls._path_from_dict(d) for d in data.get("paths", [])]
         flags = [cls._flag_from_dict(d) for d in data.get("flags", [])]
diff --git a/src/preppipe_gui_pyside6/mainwindow.py b/src/preppipe_gui_pyside6/mainwindow.py
index c03dbfe..0cbab85 100644
--- a/src/preppipe_gui_pyside6/mainwindow.py
+++ b/src/preppipe_gui_pyside6/mainwindow.py
@@ -8,6 +8,7 @@
 from .toolwidgets.home import *
 from .toolwidgets.setting import *
 from .toolwidgets.execute import *
+from .toolwidgets.scriptfloweditor import ScriptFlowEditorWidget
 from .mainwindowinterface import *
 from .guiassets import *
 
@@ -16,6 +17,8 @@ def __init__(self):
     super(MainWindow, self).__init__()
     self.ui = Ui_MainWindow()
     self.ui.setupUi(self)
+    self.ui.actionScriptFlowEditor = QAction(self)
+    self.ui.menuFunctionality.addAction(self.ui.actionScriptFlowEditor)
     if ico_path := GUIAssetLoader.try_get_asset_path("preppipe.ico"):
       self.setWindowIcon(QIcon(ico_path))
     self.updateTextForLanguage()
@@ -26,6 +29,7 @@ def __init__(self):
     self.ui.actionSettings.triggered.connect(lambda: self.requestOpenWithType(SettingWidget))
     self.ui.actionMainPipeline.triggered.connect(lambda: self.requestOpenWithType(MainInputWidget))
     self.ui.actionNavigator.triggered.connect(lambda: self.requestOpenWithType(NavigatorWidget))
+    self.ui.actionScriptFlowEditor.triggered.connect(lambda: self.requestOpenWithType(ScriptFlowEditorWidget))
     self.ui.tabWidget.tabCloseRequested.connect(self.handleTabCloseRequest)
 
   _tr_functionality = TR_gui_mainwindow.tr("functionality",
@@ -49,6 +53,7 @@ def updateTextForLanguage(self):
     self.ui.actionSettings.setText(self.tr_toolname_settings.get())
     self.ui.actionMainPipeline.setText(self.tr_toolname_maininput.get())
     self.ui.actionNavigator.setText(self.tr_toolname_navigator.get())
+    self.ui.actionScriptFlowEditor.setText(self.tr_toolname_scriptflow_editor.get())
     self.ui.menuHelp.setTitle(self._tr_help.get())
     self.ui.actionOpenDocumentation.setText(self._tr_open_documentation.get())
 
diff --git a/src/preppipe_gui_pyside6/mainwindowinterface.py b/src/preppipe_gui_pyside6/mainwindowinterface.py
index 1efd657..3a347cd 100644
--- a/src/preppipe_gui_pyside6/mainwindowinterface.py
+++ b/src/preppipe_gui_pyside6/mainwindowinterface.py
@@ -66,3 +66,8 @@ def handleLanguageChange(self) -> None:
     zh_cn="GUI 转换",
     zh_hk="GUI 轉換",
   )
+  tr_toolname_scriptflow_editor = TR_gui_mainwindow.tr("toolname_scriptflow_editor",
+    en="Script Flow Editor",
+    zh_cn="脚本流程编辑器",
+    zh_hk="腳本流程編輯器",
+  )
diff --git a/src/preppipe_gui_pyside6/toolwidgets/home.py b/src/preppipe_gui_pyside6/toolwidgets/home.py
index 72215fe..4f3a00e 100644
--- a/src/preppipe_gui_pyside6/toolwidgets/home.py
+++ b/src/preppipe_gui_pyside6/toolwidgets/home.py
@@ -8,6 +8,7 @@
 from .maininput import MainInputWidget
 from .setting import SettingWidget
 from .assetbrowser import AssetBrowserWidget
+from .scriptfloweditor import ScriptFlowEditorWidget
 
 class HomeWidget(QWidget, ToolWidgetInterface):
   _tr_toolname_home = TR_gui_mainwindow.tr("toolname_home",
@@ -39,6 +40,7 @@ class HomeWidget(QWidget, ToolWidgetInterface):
     MainInputWidget,
     SettingWidget,
     AssetBrowserWidget,
+    ScriptFlowEditorWidget,
   ]
   @classmethod
   def getToolInfo(cls) -> ToolWidgetInfo:
diff --git a/src/preppipe_gui_pyside6/toolwidgets/scriptfloweditor.py b/src/preppipe_gui_pyside6/toolwidgets/scriptfloweditor.py
new file mode 100644
index 0000000..27670f2
--- /dev/null
+++ b/src/preppipe_gui_pyside6/toolwidgets/scriptfloweditor.py
@@ -0,0 +1,953 @@
+# -*- coding: utf-8 -*-
+"""脚本流程编辑器工具：嵌入 NodeGraphQt-PySide6，与 ScriptFlowEditor 数据模型双向同步。"""
+
+from __future__ import annotations
+
+import json
+import re
+from pathlib import Path
+
+from PySide6.QtCore import Qt, QPointF
+from PySide6.QtGui import QAction, QKeySequence, QShortcut
+from PySide6.QtWidgets import (
+    QButtonGroup,
+    QCheckBox,
+    QDialog,
+    QDialogButtonBox,
+    QDoubleSpinBox,
+    QFileDialog,
+    QFormLayout,
+    QGraphicsProxyWidget,
+    QHBoxLayout,
+    QMenu,
+    QPlainTextEdit,
+    QPushButton,
+    QRadioButton,
+    QSpinBox,
+    QVBoxLayout,
+    QWidget,
+)
+from NodeGraphQt import BaseNode, NodeGraph
+from NodeGraphQt.qgraphics.pipe import PipeItem
+
+from ScriptFlowEditor.models import (
+    FlagType,
+    FlagVariable,
+    GameScriptFlow,
+    SegmentPath,
+    StorySegment,
+)
+
+from preppipe.language import TranslationDomain
+
+from ..mainwindowinterface import MainWindowInterface
+from ..toolwidgetinterface import ToolWidgetInterface, ToolWidgetInfo, ToolWidgetUniqueLevel
+
+# 节点类型标识
+_SEGMENT_NODE_TYPE = "preppipe.scriptflow.SegmentNode"
+_FLAG_NODE_TYPE = "preppipe.scriptflow.FlagNode"
+
+TR_gui_scriptfloweditor = TranslationDomain("gui_scriptfloweditor")
+
+
+class SegmentContentEditDialog(QDialog):
+    """编辑剧情段落正文的对话框，关闭时若接受则把文本写入对应 StorySegment.content。"""
+
+    def __init__(
+        self,
+        parent: QWidget | None,
+        segment: StorySegment,
+        *,
+        title: str = "",
+        placeholder: str = "",
+    ):
+        super().__init__(parent)
+        self._segment = segment
+        self.setWindowTitle(title or ("Edit Segment Content" if not parent else parent.tr("编辑剧情文本")))
+        layout = QVBoxLayout(self)
+        self._text_edit = QPlainTextEdit(self)
+        self._text_edit.setPlaceholderText(placeholder or ("Enter segment content…" if not parent else parent.tr("在此输入该段落的剧情文本…")))
+        self._text_edit.setPlainText(segment.content or "")
+        layout.addWidget(self._text_edit)
+        self._buttons = QDialogButtonBox(
+            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+        )
+        self._buttons.accepted.connect(self.accept)
+        self._buttons.rejected.connect(self.reject)
+        layout.addWidget(self._buttons)
+
+    def accept(self) -> None:
+        self._segment.content = self._text_edit.toPlainText()
+        super().accept()
+
+
+class SegmentCommentEditDialog(QDialog):
+    """编辑剧情段落注释的对话框，关闭时若接受则把文本写入对应 StorySegment.comment。"""
+
+    def __init__(
+        self,
+        parent: QWidget | None,
+        segment: StorySegment,
+        *,
+        title: str = "",
+        placeholder: str = "",
+    ):
+        super().__init__(parent)
+        self._segment = segment
+        self.setWindowTitle(title or "Edit Comment")
+        layout = QVBoxLayout(self)
+        self._text_edit = QPlainTextEdit(self)
+        self._text_edit.setPlaceholderText(placeholder or "Enter comment…")
+        self._text_edit.setPlainText(segment.comment or "")
+        layout.addWidget(self._text_edit)
+        self._buttons = QDialogButtonBox(
+            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+        )
+        self._buttons.accepted.connect(self.accept)
+        self._buttons.rejected.connect(self.reject)
+        layout.addWidget(self._buttons)
+
+    def accept(self) -> None:
+        self._segment.comment = self._text_edit.toPlainText()
+        super().accept()
+
+
+class PathConditionEditDialog(QDialog):
+    """编辑连线分支条件的对话框，关闭时若接受则把文本写入对应 SegmentPath.condition_expression。"""
+
+    def __init__(
+        self,
+        parent: QWidget | None,
+        segment_path: SegmentPath,
+        *,
+        title: str = "",
+        placeholder: str = "",
+    ):
+        super().__init__(parent)
+        self._path = segment_path
+        self.setWindowTitle(title or "Edit Branch Condition")
+        layout = QVBoxLayout(self)
+        self._text_edit = QPlainTextEdit(self)
+        self._text_edit.setPlaceholderText(placeholder or "Enter condition expression…")
+        self._text_edit.setPlainText(segment_path.condition_expression or "")
+        layout.addWidget(self._text_edit)
+        self._buttons = QDialogButtonBox(
+            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+        )
+        self._buttons.accepted.connect(self.accept)
+        self._buttons.rejected.connect(self.reject)
+        layout.addWidget(self._buttons)
+
+    def accept(self) -> None:
+        self._path.condition_expression = self._text_edit.toPlainText()
+        super().accept()
+
+
+class FlagCommentEditDialog(QDialog):
+    """编辑 Flag 变量注释的对话框，关闭时若接受则把文本写入对应 FlagVariable.comment。"""
+
+    def __init__(
+        self,
+        parent: QWidget | None,
+        flag_var: FlagVariable,
+        *,
+        title: str = "",
+        placeholder: str = "",
+    ):
+        super().__init__(parent)
+        self._flag_var = flag_var
+        self.setWindowTitle(title or "Edit Comment")
+        layout = QVBoxLayout(self)
+        self._text_edit = QPlainTextEdit(self)
+        self._text_edit.setPlaceholderText(placeholder or "Enter comment…")
+        self._text_edit.setPlainText(flag_var.comment or "")
+        layout.addWidget(self._text_edit)
+        self._buttons = QDialogButtonBox(
+            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+        )
+        self._buttons.accepted.connect(self.accept)
+        self._buttons.rejected.connect(self.reject)
+        layout.addWidget(self._buttons)
+
+    def accept(self) -> None:
+        self._flag_var.comment = self._text_edit.toPlainText()
+        super().accept()
+
+
+class FlagInitialValueEditDialog(QDialog):
+    """编辑 Flag 变量初始值的对话框，根据 flag_type 显示布尔单选/整数/浮点数输入。"""
+
+    def __init__(
+        self,
+        parent: QWidget | None,
+        flag_var: FlagVariable,
+        *,
+        title: str = "",
+    ):
+        super().__init__(parent)
+        self._flag_var = flag_var
+        self.setWindowTitle(title or "Edit Flag Initial Value")
+        layout = QFormLayout(self)
+        if flag_var.flag_type == FlagType.BOOL:
+            group = QButtonGroup(self)
+            self._radio_true = QRadioButton("True", self)
+            self._radio_false = QRadioButton("False", self)
+            group.addButton(self._radio_true)
+            group.addButton(self._radio_false)
+            self._radio_true.setChecked(bool(flag_var.initial_value))
+            self._radio_false.setChecked(not bool(flag_var.initial_value))
+            bool_row = QWidget(self)
+            bool_layout = QHBoxLayout(bool_row)
+            bool_layout.setContentsMargins(0, 0, 0, 0)
+            bool_layout.addWidget(self._radio_true)
+            bool_layout.addWidget(self._radio_false)
+            self._widget = bool_row
+        elif flag_var.flag_type == FlagType.INT:
+            self._widget = QSpinBox(self)
+            self._widget.setRange(-(2**31), 2**31 - 1)
+            self._widget.setValue(int(flag_var.initial_value))
+        else:
+            self._widget = QDoubleSpinBox(self)
+            self._widget.setDecimals(6)
+            self._widget.setRange(-1e308, 1e308)
+            self._widget.setValue(float(flag_var.initial_value))
+        layout.addRow(self._widget)
+        self._buttons = QDialogButtonBox(
+            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+        )
+        self._buttons.accepted.connect(self.accept)
+        self._buttons.rejected.connect(self.reject)
+        layout.addRow(self._buttons)
+
+    def accept(self) -> None:
+        w = self._widget
+        if self._flag_var.flag_type == FlagType.BOOL:
+            self._flag_var.initial_value = self._radio_true.isChecked()
+        elif self._flag_var.flag_type == FlagType.INT:
+            self._flag_var.initial_value = w.value()
+        else:
+            self._flag_var.initial_value = w.value()
+        super().accept()
+
+
+class SegmentNode(BaseNode):
+    """剧情段落节点：一个输入、一个输出，对应数据层 StorySegment。"""
+    __identifier__ = "preppipe.scriptflow"
+    NODE_NAME = "Segment"
+
+    def __init__(self):
+        super().__init__()
+        self.add_input("in", multi_input=True, display_name=True)
+        self.add_output("out", display_name=True)
+
+
+class FlagNode(BaseNode):
+    """Flag 节点：无端口、不允许连线，对应数据层 FlagVariable；中间显示 initial_value。"""
+    __identifier__ = "preppipe.scriptflow"
+    NODE_NAME = "Flag"
+
+    def __init__(self):
+        super().__init__()
+        # 不添加 input/output，节点不可连线
+        self.add_text_input("initial_value_display", label="", text="—")
+        w = self.get_widget("initial_value_display")
+        le = w.get_custom_widget()
+        le.setReadOnly(True)
+        le.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)  # 避免关闭对话框后弹出 QLineEdit 默认菜单（Select All 等）
+        QGraphicsProxyWidget.setToolTip(w, "")  # 禁用显示初始值的控件的 tooltip（避免库默认加属性名）
+
+
+class ScriptFlowEditorWidget(QWidget, ToolWidgetInterface):
+    """脚本流程编辑器 Tab：内嵌节点图，与 GameScriptFlow 数据层双向同步。"""
+
+    # 多语言：与 setting、home 等一致，使用 TranslationDomain + bind_text，语言切换时由 update_text 更新
+    # _tr_add_node = TR_gui_scriptfloweditor.tr("add_node", en="Add Node", zh_cn="添加节点", zh_hk="添加節點")
+    _tr_add_script_node = TR_gui_scriptfloweditor.tr("add_script_node", en="Add Script Node", zh_cn="增加剧情节点", zh_hk="增加劇情節點")
+    _tr_add_flag = TR_gui_scriptfloweditor.tr("add_flag_variable", en="Add Flag Variable", zh_cn="添加Flag变量", zh_hk="添加Flag變量")
+    # _tr_btn_add_flag = TR_gui_scriptfloweditor.tr("btn_add_flag", en="Add Flag", zh_cn="添加Flag", zh_hk="添加Flag")
+    _tr_btn_add_flag_tooltip = TR_gui_scriptfloweditor.tr("btn_add_flag_tooltip", en="Add a flag variable node.", zh_cn="添加一个 Flag 变量节点", zh_hk="添加一個 Flag 變量節點")
+    _tr_edit_segment_content = TR_gui_scriptfloweditor.tr("edit_segment_content", en="Edit Segment Content", zh_cn="编辑剧情文本", zh_hk="編輯劇情文本")
+    _tr_edit_comment = TR_gui_scriptfloweditor.tr("edit_comment", en="Edit Comment", zh_cn="编辑注释", zh_hk="編輯註釋")
+    _tr_edit_dialog_title = TR_gui_scriptfloweditor.tr("edit_dialog_title", en="Edit Segment Content", zh_cn="编辑剧情文本", zh_hk="編輯劇情文本")
+    _tr_edit_comment_dialog_title = TR_gui_scriptfloweditor.tr("edit_comment_dialog_title", en="Edit Comment", zh_cn="编辑注释", zh_hk="編輯註釋")
+    _tr_edit_comment_dialog_placeholder = TR_gui_scriptfloweditor.tr("edit_comment_dialog_placeholder", en="Enter comment for this node…", zh_cn="在此输入该节点的注释…", zh_hk="在此輸入該節點的註釋…")
+    _tr_edit_dialog_placeholder = TR_gui_scriptfloweditor.tr("edit_dialog_placeholder", en="Enter the segment story text here…", zh_cn="在此输入该段落的剧情文本…", zh_hk="在此輸入該段落的劇情文本…")
+    # _tr_btn_add = TR_gui_scriptfloweditor.tr("btn_add", en="Add Segment Node", zh_cn="增加剧情节点", zh_hk="增加劇情節點")
+    _tr_btn_add_tooltip = TR_gui_scriptfloweditor.tr("btn_add_tooltip", en="Add a new segment node at the top-left of the view.", zh_cn="当前视图左上角新增一个剧情节点", zh_hk="當前視圖左上角新增一個劇情節點")
+    _tr_btn_del = TR_gui_scriptfloweditor.tr("btn_del", en="Delete Selected", zh_cn="删除选中节点", zh_hk="刪除選中節點")
+    _tr_btn_del_tooltip = TR_gui_scriptfloweditor.tr("btn_del_tooltip", en="Delete the selected nodes.", zh_cn="删除当前选中节点", zh_hk="刪除當前選中節點")
+    _tr_chk_acyclic = TR_gui_scriptfloweditor.tr("chk_acyclic", en="Acyclic", zh_cn="无环模式", zh_hk="無環模式")
+    _tr_chk_acyclic_tooltip = TR_gui_scriptfloweditor.tr("chk_acyclic_tooltip", en="When checked, cycles are not allowed.", zh_cn="勾选后，禁止创建闭环连接", zh_hk="勾選後，禁止創建閉環連接")
+    _tr_save_flow = TR_gui_scriptfloweditor.tr("save_flow", en="Save Flow", zh_cn="保存流程", zh_hk="保存流程")
+    _tr_save_tooltip = TR_gui_scriptfloweditor.tr("save_tooltip", en="Save current flow and node positions to a JSON file.", zh_cn="将当前流程与节点位置保存为 JSON 文件", zh_hk="將當前流程與節點位置保存為 JSON 文件")
+    _tr_load_flow = TR_gui_scriptfloweditor.tr("load_flow", en="Load Flow", zh_cn="打开流程", zh_hk="打開流程")
+    _tr_open_tooltip = TR_gui_scriptfloweditor.tr("open_tooltip", en="Load flow and node positions from a JSON file.", zh_cn="从 JSON 文件加载流程与节点位置", zh_hk="從 JSON 文件加載流程與節點位置")
+    _tr_save_dialog_title = TR_gui_scriptfloweditor.tr("save_dialog_title", en="Save Flow", zh_cn="保存流程", zh_hk="保存流程")
+    _tr_open_dialog_title = TR_gui_scriptfloweditor.tr("open_dialog_title", en="Open Flow", zh_cn="打开流程", zh_hk="打開流程")
+    _tr_json_filter = TR_gui_scriptfloweditor.tr("json_filter", en="JSON files (*.json);;All files (*)", zh_cn="JSON 文件 (*.json);;所有文件 (*)", zh_hk="JSON 文件 (*.json);;所有文件 (*)")
+
+    _tr_node_tooltip_ending = TR_gui_scriptfloweditor.tr("node_tooltip_ending", en="Ending segment.", zh_cn="剧本结局节点", zh_hk="劇本結局節點")
+    _tr_node_tooltip_start = TR_gui_scriptfloweditor.tr("node_tooltip_start", en="Start segment.", zh_cn="剧本起始节点", zh_hk="劇本起始節點")
+    _tr_node_tooltip_default = TR_gui_scriptfloweditor.tr("node_tooltip_default", en="Segment node.", zh_cn="新剧情段落节点", zh_hk="新劇情段落節點")
+    _tr_node_tooltip_flag_default = TR_gui_scriptfloweditor.tr("node_tooltip_flag_default", en="Flag variable node.", zh_cn="Flag 变量节点", zh_hk="Flag 變量節點")
+    _tr_node_name_edit_tooltip = TR_gui_scriptfloweditor.tr("node_name_edit_tooltip", en="Double-click to edit node name.", zh_cn="双击编辑节点名称", zh_hk="雙擊編輯節點名稱")
+
+    _tr_flag_change_type = TR_gui_scriptfloweditor.tr("flag_change_type", en="Change Flag Type", zh_cn="修改flag变量类型", zh_hk="修改flag變量類型")
+    _tr_flag_type_bool = TR_gui_scriptfloweditor.tr("flag_type_bool", en="Boolean", zh_cn="布尔型", zh_hk="布爾型")
+    _tr_flag_type_int = TR_gui_scriptfloweditor.tr("flag_type_int", en="Integer", zh_cn="整型", zh_hk="整型")
+    _tr_flag_type_float = TR_gui_scriptfloweditor.tr("flag_type_float", en="Float", zh_cn="浮点型", zh_hk="浮點型")
+    _tr_flag_change_initial = TR_gui_scriptfloweditor.tr("flag_change_initial", en="Change Flag Initial Value", zh_cn="修改flag初始值", zh_hk="修改flag初始值")
+    _tr_flag_initial_dialog_title = TR_gui_scriptfloweditor.tr("flag_initial_dialog_title", en="Edit Flag Initial Value", zh_cn="修改flag初始值", zh_hk="修改flag初始值")
+
+    _tr_edit_branch_condition = TR_gui_scriptfloweditor.tr("edit_branch_condition", en="Edit Branch Condition", zh_cn="编辑分支条件", zh_hk="編輯分支條件")
+    _tr_edit_branch_condition_dialog_title = TR_gui_scriptfloweditor.tr("edit_branch_condition_dialog_title", en="Edit Branch Condition", zh_cn="编辑分支条件", zh_hk="編輯分支條件")
+    _tr_edit_branch_condition_placeholder = TR_gui_scriptfloweditor.tr("edit_branch_condition_placeholder", en="Enter condition expression for this path…", zh_cn="在此输入该连线的条件表达式…", zh_hk="在此輸入該連線的條件表達式…")
+
+    _tr_toolname = MainWindowInterface.tr_toolname_scriptflow_editor
+
+    @classmethod
+    def getToolInfo(cls) -> ToolWidgetInfo:
+        return ToolWidgetInfo(
+            idstr="scriptflow_editor",
+            name=ScriptFlowEditorWidget._tr_toolname,
+            widget=cls,
+            uniquelevel=ToolWidgetUniqueLevel.SINGLE_INSTANCE,
+        )
+
+    def __init__(self, parent: QWidget | None = None):
+        super().__init__(parent)
+        main_layout = QVBoxLayout(self)
+        main_layout.setContentsMargins(0, 0, 0, 0)
+
+        self._graph = NodeGraph(parent=self)
+        self._graph.register_node(SegmentNode)
+        self._graph.register_node(FlagNode)
+        self._graph.set_acyclic(True)
+
+        # 数据层：与节点视图一一对应
+        self._flow = GameScriptFlow(name="ScriptFlow")
+        self._node_id_to_segment_id: dict[str, str] = {}
+        self._node_id_to_flag_id: dict[str, str] = {}
+        # 为 True 时表示正在根据 flow 构建图，不把连线/断开同步回 flow，避免重复路径与误判闭环
+        self._building_from_flow = False
+
+        # 视图 → 数据：连线、删除、改名
+        self._graph.port_connected.connect(self._on_port_connected)
+        self._graph.port_disconnected.connect(self._on_port_disconnected)
+        self._graph.nodes_deleted.connect(self._on_nodes_deleted)
+        self._graph.viewer().node_name_changed.connect(self._on_node_name_changed)
+
+        # 在节点图右键菜单中增加「Add Node」；右键空白处时在点击位置创建节点；右键节点时记录节点 id 供 Flag 子菜单使用
+        self._last_context_menu_scene_pos = None
+        self._last_context_menu_node_id: str | None = None
+        # 右键前缓存的选中连线（右键后 selection 可能被清空，用于「编辑分支条件」）
+        self._context_menu_pipe_selection: list = []
+        self._graph.context_menu_prompt.connect(self._on_context_menu_prompt)
+        graph_menu = self._graph.viewer().context_menus()["graph"]
+        self._ctx_action_add_node = QAction(self._tr_add_script_node.get(), self)
+        self._ctx_action_add_node.triggered.connect(self._on_add_node)
+        graph_menu.addAction(self._ctx_action_add_node)
+        self.bind_text(self._ctx_action_add_node.setText, self._tr_add_script_node)
+        self._ctx_action_add_flag = QAction(self._tr_add_flag.get(), self)
+        self._ctx_action_add_flag.triggered.connect(self._on_add_flag)
+        graph_menu.addAction(self._ctx_action_add_flag)
+        self.bind_text(self._ctx_action_add_flag.setText, self._tr_add_flag)
+        self._ctx_action_edit_path_condition = QAction(self._tr_edit_branch_condition.get(), self)
+        self._ctx_action_edit_path_condition.triggered.connect(self._on_edit_path_condition)
+        graph_menu.addAction(self._ctx_action_edit_path_condition)
+        self.bind_text(self._ctx_action_edit_path_condition.setText, self._tr_edit_branch_condition)
+
+        # 节点右键菜单：编辑剧情文本（保留 command 引用以便 update_text 时更新文案）
+        nodes_menu = self._graph.get_context_menu("nodes")
+        self._ctx_command_edit_content = nodes_menu.add_command(
+            self._tr_edit_segment_content.get(),
+            func=self._on_edit_segment_content,
+            node_class=SegmentNode,
+        )
+        self._ctx_command_edit_comment = nodes_menu.add_command(
+            self._tr_edit_comment.get(),
+            func=self._on_edit_segment_comment,
+            node_class=SegmentNode,
+        )
+        # FlagNode 右键菜单：「修改flag变量类型」二级菜单（布尔型/整型/浮点型）、「修改flag初始值」
+        self._ctx_command_edit_flag_initial = nodes_menu.add_command(
+            self._tr_flag_change_initial.get(),
+            func=self._on_edit_flag_initial_value,
+            node_class=FlagNode,
+        )
+        self._ctx_command_edit_flag_comment = nodes_menu.add_command(
+            self._tr_edit_comment.get(),
+            func=self._on_edit_flag_comment,
+            node_class=FlagNode,
+        )
+        # 在 FlagNode 子菜单前插入「修改flag变量类型」二级菜单（库只对直接 action 设置 node_id，子菜单项用 _last_context_menu_node_id）
+        nodes_qmenu = nodes_menu.qmenu
+        for action in nodes_qmenu.actions():
+            sub = action.menu()
+            if sub is not None and getattr(sub, "node_class", None) is FlagNode:
+                type_submenu = QMenu(self._tr_flag_change_type.get(), self)
+                for label_tr, flag_type in [
+                    (self._tr_flag_type_bool, FlagType.BOOL),
+                    (self._tr_flag_type_int, FlagType.INT),
+                    (self._tr_flag_type_float, FlagType.FLOAT),
+                ]:
+                    a = QAction(label_tr.get(), self)
+                    a.triggered.connect(lambda checked=False, ft=flag_type: self._on_set_flag_type(ft))
+                    type_submenu.addAction(a)
+                sub.insertMenu(sub.actions()[0], type_submenu)
+                self._ctx_menu_flag_type = type_submenu
+                break
+        else:
+            self._ctx_menu_flag_type = None
+
+        # 工具栏：所有文案用 bind_text 绑定，语言切换时由 update_text 更新
+        toolbar = QWidget()
+        bar_layout = QHBoxLayout(toolbar)
+        bar_layout.setContentsMargins(4, 2, 4, 2)
+        self._btn_add = QPushButton(self._tr_add_script_node.get())
+        self._btn_add.clicked.connect(self._on_add_node)
+        self.bind_text(self._btn_add.setText, self._tr_add_script_node)
+        self.bind_text(self._btn_add.setToolTip, self._tr_btn_add_tooltip)
+        self._btn_del = QPushButton(self._tr_btn_del.get())
+        self._btn_del.clicked.connect(self._on_delete_selected)
+        self.bind_text(self._btn_del.setText, self._tr_btn_del)
+        self.bind_text(self._btn_del.setToolTip, self._tr_btn_del_tooltip)
+        self._chk_acyclic = QCheckBox(self._tr_chk_acyclic.get())
+        self._chk_acyclic.setChecked(True)
+        self._chk_acyclic.toggled.connect(self._on_acyclic_toggled)
+        self.bind_text(self._chk_acyclic.setText, self._tr_chk_acyclic)
+        self.bind_text(self._chk_acyclic.setToolTip, self._tr_chk_acyclic_tooltip)
+        self._btn_save = QPushButton(self._tr_save_flow.get())
+        self._btn_save.clicked.connect(self._on_save)
+        self.bind_text(self._btn_save.setText, self._tr_save_flow)
+        self.bind_text(self._btn_save.setToolTip, self._tr_save_tooltip)
+        self._btn_load = QPushButton(self._tr_load_flow.get())
+        self._btn_load.clicked.connect(self._on_load)
+        self.bind_text(self._btn_load.setText, self._tr_load_flow)
+        self.bind_text(self._btn_load.setToolTip, self._tr_open_tooltip)
+        self._btn_add_flag = QPushButton(self._tr_add_flag.get())
+        self._btn_add_flag.clicked.connect(self._on_add_flag)
+        self.bind_text(self._btn_add_flag.setText, self._tr_add_flag)
+        self.bind_text(self._btn_add_flag.setToolTip, self._tr_btn_add_flag_tooltip)
+        bar_layout.addWidget(self._btn_add)
+        bar_layout.addWidget(self._btn_add_flag)
+        bar_layout.addWidget(self._btn_del)
+        bar_layout.addWidget(self._chk_acyclic)
+        bar_layout.addWidget(self._btn_save)
+        bar_layout.addWidget(self._btn_load)
+        bar_layout.addStretch()
+        main_layout.addWidget(toolbar)
+
+        # 从数据层构建初始图：Start → Ending
+        self._build_initial_flow()
+        self._build_graph_from_flow()
+
+        main_layout.addWidget(self._graph.widget)
+        for key in (Qt.Key.Key_Delete, Qt.Key.Key_Backspace):
+            shortcut = QShortcut(QKeySequence(key), self, context=Qt.ShortcutContext.WidgetWithChildrenShortcut)
+            shortcut.activated.connect(self._on_delete_key)
+
+    def update_text(self) -> None:
+        super().update_text()
+        # 图右键菜单项需随语言更新
+        self._ctx_action_add_node.setText(self._tr_add_script_node.get())
+        self._ctx_action_add_flag.setText(self._tr_add_flag.get())
+        self._ctx_action_edit_path_condition.setText(self._tr_edit_branch_condition.get())
+        # 节点右键菜单由库管理，需手动更新
+        self._ctx_command_edit_content.qaction.setText(self._tr_edit_segment_content.get())
+        self._ctx_command_edit_comment.qaction.setText(self._tr_edit_comment.get())
+        self._ctx_command_edit_flag_initial.qaction.setText(self._tr_flag_change_initial.get())
+        self._ctx_command_edit_flag_comment.qaction.setText(self._tr_edit_comment.get())
+        if hasattr(self, "_ctx_menu_flag_type") and self._ctx_menu_flag_type is not None:
+            self._ctx_menu_flag_type.setTitle(self._tr_flag_change_type.get())
+            for action, label_tr in zip(
+                self._ctx_menu_flag_type.actions(),
+                (self._tr_flag_type_bool, self._tr_flag_type_int, self._tr_flag_type_float),
+            ):
+                action.setText(label_tr.get())
+        # 所有节点及名称区的 tooltip 随语言更新
+        for node in self._graph.all_nodes():
+            seg_id = self._node_id_to_segment_id.get(node.id)
+            if seg_id is not None:
+                seg = self._flow.get_segment_by_id(seg_id)
+                if seg is not None:
+                    self._set_node_tooltips(node, seg)
+                continue
+            flag_id = self._node_id_to_flag_id.get(node.id)
+            if flag_id is not None:
+                flag_var = self._flow.get_flag_by_id(flag_id)
+                if flag_var is not None:
+                    self._set_flag_node_tooltips(node, flag_var)
+
+    def _build_initial_flow(self) -> None:
+        """数据层初始状态：Start 段落、Ending 段落、一条路径。"""
+        seg_start = StorySegment(name="Start", content="\"剧情从这里开始。\"", is_ending_segment=False)
+        seg_ending = StorySegment(name="Ending", content="\"剧情在这里结束。\"", is_ending_segment=True)
+        self._flow.segments = [seg_start, seg_ending]
+        path = SegmentPath(prev_segment_id=seg_start.id, next_segment_id=seg_ending.id)
+        self._flow.paths = [path]
+        seg_start.add_path_segment_id(path.id, seg_ending.id)
+
+    def _build_graph_from_flow(
+        self,
+        segment_positions: dict[str, list[float]] | None = None,
+        flag_positions: dict[str, list[float]] | None = None,
+    ) -> None:
+        """根据当前 flow 在图中创建节点与连线；segment_positions / flag_positions 用于恢复位置。"""
+        self._building_from_flow = True
+        try:
+            self._build_graph_from_flow_impl(
+                segment_positions=segment_positions,
+                flag_positions=flag_positions,
+            )
+        finally:
+            self._building_from_flow = False
+
+    def _build_graph_from_flow_impl(
+        self,
+        segment_positions: dict[str, list[float]] | None = None,
+        flag_positions: dict[str, list[float]] | None = None,
+    ) -> None:
+        segment_positions = segment_positions or {}
+        flag_positions = flag_positions or {}
+        default_positions = [(0, 0), (400, 0)]
+        for i, seg in enumerate(self._flow.segments):
+            pos = segment_positions.get(seg.id)
+            if pos is not None and len(pos) >= 2:
+                pos = (float(pos[0]), float(pos[1]))
+            else:
+                pos = default_positions[i] if i < len(default_positions) else (0, 0)
+            node = self._graph.create_node(
+                _SEGMENT_NODE_TYPE,
+                name=seg.name,
+                pos=pos,
+            )
+            self._node_id_to_segment_id[node.id] = seg.id
+            self._set_node_tooltips(node, seg)
+        for f in self._flow.flags:
+            pos = flag_positions.get(f.id)
+            if pos is not None and len(pos) >= 2:
+                pos = (float(pos[0]), float(pos[1]))
+            else:
+                pos = (0, 0)
+            node = self._graph.create_node(
+                _FLAG_NODE_TYPE,
+                name=f.name,
+                pos=pos,
+            )
+            self._node_id_to_flag_id[node.id] = f.id
+            self._update_flag_node_display(node)
+            self._set_flag_node_tooltips(node, f)
+        seg_id_to_node = {
+            self._node_id_to_segment_id[n.id]: n
+            for n in self._graph.all_nodes()
+            if n.id in self._node_id_to_segment_id
+        }
+        for path in self._flow.paths:
+            prev_node = seg_id_to_node.get(path.prev_segment_id)
+            next_node = seg_id_to_node.get(path.next_segment_id)
+            if prev_node is not None and next_node is not None:
+                prev_node.set_output(0, next_node.input(0))
+        self._update_pipe_tooltips()
+
+    def _update_pipe_tooltips(self) -> None:
+        """根据 SegmentPath.condition_expression 刷新场景中所有连线的 tooltip。"""
+        scene = self._graph.viewer().scene()
+        for item in scene.items():
+            if not isinstance(item, PipeItem):
+                continue
+            if not item.input_port or not item.output_port:
+                continue
+            prev_seg_id = self._node_id_to_segment_id.get(item.output_port.node.id)
+            next_seg_id = self._node_id_to_segment_id.get(item.input_port.node.id)
+            if prev_seg_id is None or next_seg_id is None:
+                item.setToolTip("")
+                continue
+            path = next(
+                (p for p in self._flow.paths if p.prev_segment_id == prev_seg_id and p.next_segment_id == next_seg_id),
+                None,
+            )
+            text = (path.condition_expression or "").strip() if path is not None else ""
+            item.setToolTip(text)
+
+    def _set_node_tooltips(self, node, segment: StorySegment) -> None:
+        """根据段落类型或注释设置节点与名称区的 tooltip（使用当前语言）。有注释时节点 tooltip 显示注释。"""
+        if segment.comment and segment.comment.strip():
+            node.view.setToolTip(segment.comment.strip())
+        elif segment.is_ending_segment:
+            node.view.setToolTip(self._tr_node_tooltip_ending.get())
+        elif segment.name == "Start":
+            node.view.setToolTip(self._tr_node_tooltip_start.get())
+        else:
+            node.view.setToolTip(self._tr_node_tooltip_default.get())
+        node.view.text_item.setToolTip(self._tr_node_name_edit_tooltip.get())
+
+    def _set_flag_node_tooltips(self, node, flag_var: FlagVariable) -> None:
+        """根据 FlagVariable.comment 或当前语言默认文案设置 Flag 节点及名称区的 tooltip。"""
+        if flag_var.comment and flag_var.comment.strip():
+            node.view.setToolTip(flag_var.comment.strip())
+        else:
+            node.view.setToolTip(self._tr_node_tooltip_flag_default.get())
+        node.view.text_item.setToolTip(self._tr_node_name_edit_tooltip.get())
+
+    def _format_flag_initial_value(self, flag_var: FlagVariable) -> str:
+        """将 FlagVariable.initial_value 格式化为节点中部显示的字符串。"""
+        if flag_var.flag_type == FlagType.BOOL:
+            return "True" if flag_var.initial_value else "False"
+        if flag_var.flag_type == FlagType.INT:
+            return str(int(flag_var.initial_value))
+        return str(float(flag_var.initial_value))
+
+    def _update_flag_node_display(self, node) -> None:
+        """根据节点对应的 FlagVariable 更新节点中部的 initial_value 显示。"""
+        flag_id = self._node_id_to_flag_id.get(node.id)
+        if flag_id is None:
+            return
+        flag_var = self._flow.get_flag_by_id(flag_id)
+        if flag_var is None:
+            return
+        if node.view.has_widget("initial_value_display"):
+            node.get_widget("initial_value_display").set_value(
+                self._format_flag_initial_value(flag_var)
+            )
+
+    def _next_segment_name(self) -> str:
+        """生成下一个 segment_<index> 名称，保证不与已有节点重名。"""
+        pattern = re.compile(r"^segment_(\d+)$")
+        max_index = -1
+        for node in self._graph.all_nodes():
+            m = pattern.match(node.name())
+            if m:
+                max_index = max(max_index, int(m.group(1)))
+        return "segment_{}".format(max_index + 1)
+
+    def _next_flag_name(self) -> str:
+        """生成下一个 flag_<index> 名称，保证不与已有节点重名。"""
+        pattern = re.compile(r"^flag_(\d+)$")
+        max_index = -1
+        for node in self._graph.all_nodes():
+            m = pattern.match(node.name())
+            if m:
+                max_index = max(max_index, int(m.group(1)))
+        return "flag_{}".format(max_index + 1)
+
+    def _on_context_menu_prompt(self, _menu: object, node: object) -> None:
+        """右键菜单即将弹出时记录场景坐标，供「Add Node」在点击位置创建节点；记录节点 id 供 Flag 子菜单使用；图菜单时根据光标下或已选中的单条连线启用「编辑分支条件」。"""
+        if node is None:
+            pos = self._graph.viewer().scene_cursor_pos()
+            self._last_context_menu_scene_pos = (pos.x(), pos.y())
+            self._last_context_menu_node_id = None
+            # 优先用当前选中连线；若无则取光标下的连线（右键后 selection 常被清空）
+            self._context_menu_pipe_selection = list(self._graph.selected_pipes())
+            if len(self._context_menu_pipe_selection) != 1:
+                scene = self._graph.viewer().scene()
+                pt = QPointF(pos.x(), pos.y())
+                items_at = scene.items(pt)
+                pipes_at = [i for i in items_at if isinstance(i, PipeItem)]
+                if len(pipes_at) == 1:
+                    old_sel = list(scene.selectedItems())
+                    scene.clearSelection()
+                    pipes_at[0].setSelected(True)
+                    self._context_menu_pipe_selection = list(self._graph.selected_pipes())
+                    scene.clearSelection()
+                    for item in old_sel:
+                        item.setSelected(True)
+            self._ctx_action_edit_path_condition.setEnabled(len(self._context_menu_pipe_selection) == 1)
+        else:
+            self._last_context_menu_scene_pos = None
+            self._last_context_menu_node_id = node.id
+
+    def _on_add_node(self) -> None:
+        if self._last_context_menu_scene_pos is not None:
+            pos = self._last_context_menu_scene_pos
+            self._last_context_menu_scene_pos = None
+        else:
+            pos = (0, 0)
+        name = self._next_segment_name()
+        segment = StorySegment(name=name, content="", is_ending_segment=False)
+        self._flow.segments.append(segment)
+        node = self._graph.create_node(_SEGMENT_NODE_TYPE, name=name, pos=pos)
+        self._node_id_to_segment_id[node.id] = segment.id
+        self._set_node_tooltips(node, segment)
+
+    def _on_add_flag(self) -> None:
+        """在图中添加一个 FlagNode，并在 flow.flags 中新增对应 FlagVariable。"""
+        if self._last_context_menu_scene_pos is not None:
+            pos = self._last_context_menu_scene_pos
+            self._last_context_menu_scene_pos = None
+        else:
+            pos = (0, 0)
+        name = self._next_flag_name()
+        flag_var = FlagVariable(name=name, flag_type=FlagType.BOOL, initial_value=False)
+        self._flow.flags.append(flag_var)
+        node = self._graph.create_node(_FLAG_NODE_TYPE, name=name, pos=pos)
+        self._node_id_to_flag_id[node.id] = flag_var.id
+        self._update_flag_node_display(node)
+        self._set_flag_node_tooltips(node, flag_var)
+
+    def _on_delete_selected(self) -> None:
+        self._delete_selected_pipes()
+        self._graph.delete_nodes(self._graph.selected_nodes())
+
+    def _on_acyclic_toggled(self, checked: bool) -> None:
+        self._graph.set_acyclic(checked)
+
+    def _on_delete_key(self) -> None:
+        self._on_delete_selected()
+
+    def _on_edit_path_condition(self) -> None:
+        """图右键「编辑分支条件」：对当前选中的单条连线，编辑对应 SegmentPath.condition_expression。"""
+        pipes = self._context_menu_pipe_selection if len(self._context_menu_pipe_selection) == 1 else self._graph.selected_pipes()
+        if len(pipes) != 1:
+            return
+        in_port, out_port = pipes[0]
+        prev_seg_id = self._node_id_to_segment_id.get(out_port.node().id)
+        next_seg_id = self._node_id_to_segment_id.get(in_port.node().id)
+        if prev_seg_id is None or next_seg_id is None:
+            return
+        path = next(
+            (p for p in self._flow.paths if p.prev_segment_id == prev_seg_id and p.next_segment_id == next_seg_id),
+            None,
+        )
+        if path is None:
+            return
+        dialog = PathConditionEditDialog(
+            self,
+            segment_path=path,
+            title=self._tr_edit_branch_condition_dialog_title.get(),
+            placeholder=self._tr_edit_branch_condition_placeholder.get(),
+        )
+        if dialog.exec() == QDialog.DialogCode.Accepted:
+            self._update_pipe_tooltips()
+
+    def _on_port_connected(self, in_port, out_port) -> None:
+        """连线建立：在数据层添加 SegmentPath。NodeGraphQt 信号参数顺序为 (input_port, output_port)。同一条连线只添加一次。"""
+        if self._building_from_flow:
+            return
+        out_node = out_port.node()
+        in_node = in_port.node()
+        prev_seg_id = self._node_id_to_segment_id.get(out_node.id)
+        next_seg_id = self._node_id_to_segment_id.get(in_node.id)
+        if prev_seg_id is None or next_seg_id is None:
+            return
+        already = any(
+            p.prev_segment_id == prev_seg_id and p.next_segment_id == next_seg_id
+            for p in self._flow.paths
+        )
+        if already:
+            return
+        path = SegmentPath(prev_segment_id=prev_seg_id, next_segment_id=next_seg_id)
+        self._flow.paths.append(path)
+        prev_seg = self._flow.get_segment_by_id(prev_seg_id)
+        if prev_seg is not None:
+            prev_seg.add_path_segment_id(path.id, next_seg_id)
+        self._update_pipe_tooltips()
+
+    def _on_port_disconnected(self, in_port, out_port) -> None:
+        """连线断开：从数据层移除对应 SegmentPath。NodeGraphQt 信号参数顺序为 (input_port, output_port)。"""
+        if self._building_from_flow:
+            return
+        out_node = out_port.node()
+        in_node = in_port.node()
+        prev_seg_id = self._node_id_to_segment_id.get(out_node.id)
+        next_seg_id = self._node_id_to_segment_id.get(in_node.id)
+        if prev_seg_id is None or next_seg_id is None:
+            return
+        to_remove = [
+            p for p in self._flow.paths
+            if p.prev_segment_id == prev_seg_id and p.next_segment_id == next_seg_id
+        ]
+        for p in to_remove:
+            self._flow.paths.remove(p)
+            prev_seg = self._flow.get_segment_by_id(prev_seg_id)
+            if prev_seg is not None and p.id in prev_seg.paths_segment_ids:
+                del prev_seg.paths_segment_ids[p.id]
+
+    def _on_nodes_deleted(self, node_ids: list) -> None:
+        """节点删除：从数据层移除对应段落/Flag 及关联路径，并清理映射。"""
+        for nid in node_ids:
+            seg_id = self._node_id_to_segment_id.pop(nid, None)
+            if seg_id is not None:
+                self._flow.segments = [s for s in self._flow.segments if s.id != seg_id]
+                self._flow.paths = [
+                    p for p in self._flow.paths
+                    if p.prev_segment_id != seg_id and p.next_segment_id != seg_id
+                ]
+                for s in self._flow.segments:
+                    for path_id in list(s.paths_segment_ids.keys()):
+                        if s.paths_segment_ids[path_id] == seg_id:
+                            del s.paths_segment_ids[path_id]
+                continue
+            flag_id = self._node_id_to_flag_id.pop(nid, None)
+            if flag_id is not None:
+                self._flow.flags = [f for f in self._flow.flags if f.id != flag_id]
+
+    def _on_node_name_changed(self, node_id: str, name: str) -> None:
+        """节点改名：同步到数据层 StorySegment.name 或 FlagVariable.name（id 保持不变），Segment 依 comment 重设 tooltip。"""
+        seg_id = self._node_id_to_segment_id.get(node_id)
+        if seg_id is not None:
+            seg = self._flow.get_segment_by_id(seg_id)
+            if seg is not None:
+                seg.name = name
+                node = self._graph.get_node_by_id(node_id)
+                if node is not None:
+                    self._set_node_tooltips(node, seg)
+            return
+        flag_id = self._node_id_to_flag_id.get(node_id)
+        if flag_id is not None:
+            flag_var = self._flow.get_flag_by_id(flag_id)
+            if flag_var is not None:
+                flag_var.name = name
+                node = self._graph.get_node_by_id(node_id)
+                if node is not None:
+                    self._set_flag_node_tooltips(node, flag_var)
+
+    def _on_edit_segment_content(self, _graph, node) -> None:
+        """节点右键「编辑剧情文本」：弹出对话框编辑对应 StorySegment.content，关闭时保存。"""
+        seg_id = self._node_id_to_segment_id.get(node.id)
+        if seg_id is None:
+            return
+        seg = self._flow.get_segment_by_id(seg_id)
+        if seg is None:
+            return
+        dialog = SegmentContentEditDialog(
+            self,
+            segment=seg,
+            title=self._tr_edit_dialog_title.get(),
+            placeholder=self._tr_edit_dialog_placeholder.get(),
+        )
+        dialog.exec()
+
+    def _on_edit_segment_comment(self, _graph, node) -> None:
+        """节点右键「编辑注释」：弹出对话框编辑对应 StorySegment.comment，关闭时保存并设为节点 tooltip。"""
+        seg_id = self._node_id_to_segment_id.get(node.id)
+        if seg_id is None:
+            return
+        seg = self._flow.get_segment_by_id(seg_id)
+        if seg is None:
+            return
+        dialog = SegmentCommentEditDialog(
+            self,
+            segment=seg,
+            title=self._tr_edit_comment_dialog_title.get(),
+            placeholder=self._tr_edit_comment_dialog_placeholder.get(),
+        )
+        if dialog.exec() == QDialog.DialogCode.Accepted:
+            self._set_node_tooltips(node, seg)
+
+    def _on_set_flag_type(self, new_type: FlagType) -> None:
+        """将当前右键的 Flag 节点对应的 FlagVariable 的 flag_type 设为 new_type；类型未变则不改 initial_value，类型改变则 bool→False、int/float→0。"""
+        node_id = self._last_context_menu_node_id
+        if node_id is None:
+            return
+        flag_id = self._node_id_to_flag_id.get(node_id)
+        if flag_id is None:
+            return
+        flag_var = self._flow.get_flag_by_id(flag_id)
+        if flag_var is None:
+            return
+        old_type = flag_var.flag_type
+        flag_var.flag_type = new_type
+        if old_type != new_type:
+            flag_var.initial_value = False if new_type == FlagType.BOOL else 0
+        graph_node = self._graph.get_node_by_id(node_id)
+        if graph_node is not None:
+            self._update_flag_node_display(graph_node)
+
+    def _on_edit_flag_initial_value(self, _graph, node) -> None:
+        """节点右键「修改flag初始值」：弹出对话框编辑对应 FlagVariable.initial_value。"""
+        flag_id = self._node_id_to_flag_id.get(node.id)
+        if flag_id is None:
+            return
+        flag_var = self._flow.get_flag_by_id(flag_id)
+        if flag_var is None:
+            return
+        dialog = FlagInitialValueEditDialog(
+            self,
+            flag_var=flag_var,
+            title=self._tr_flag_initial_dialog_title.get(),
+        )
+        if dialog.exec() == QDialog.DialogCode.Accepted:
+            self._update_flag_node_display(node)
+            self._set_flag_node_tooltips(node, flag_var)
+
+    def _on_edit_flag_comment(self, _graph, node) -> None:
+        """节点右键「编辑注释」：弹出对话框编辑对应 FlagVariable.comment，关闭时同步到节点 tooltip。"""
+        flag_id = self._node_id_to_flag_id.get(node.id)
+        if flag_id is None:
+            return
+        flag_var = self._flow.get_flag_by_id(flag_id)
+        if flag_var is None:
+            return
+        dialog = FlagCommentEditDialog(
+            self,
+            flag_var=flag_var,
+            title=self._tr_edit_comment_dialog_title.get(),
+            placeholder=self._tr_edit_comment_dialog_placeholder.get(),
+        )
+        if dialog.exec() == QDialog.DialogCode.Accepted:
+            self._set_flag_node_tooltips(node, flag_var)
+
+    def _on_save(self) -> None:
+        """将当前流程数据与节点位置保存为 JSON 文件。"""
+        path, _ = QFileDialog.getSaveFileName(
+            self,
+            self._tr_save_dialog_title.get(),
+            "",
+            self._tr_json_filter.get(),
+        )
+        if not path:
+            return
+        path = Path(path)
+        if path.suffix.lower() != ".json":
+            path = path.with_suffix(".json")
+        flow_dict = self._flow.to_dict()
+        node_positions = {}
+        flag_positions = {}
+        for node in self._graph.all_nodes():
+            pos = getattr(node.model, "pos", [0.0, 0.0])
+            pos_list = list(pos) if isinstance(pos, (list, tuple)) else [0.0, 0.0]
+            seg_id = self._node_id_to_segment_id.get(node.id)
+            if seg_id is not None:
+                node_positions[seg_id] = pos_list
+            else:
+                flag_id = self._node_id_to_flag_id.get(node.id)
+                if flag_id is not None:
+                    flag_positions[flag_id] = pos_list
+        flow_dict["node_positions"] = node_positions
+        flow_dict["flag_positions"] = flag_positions
+        path.write_text(
+            json.dumps(flow_dict, indent=2, ensure_ascii=False),
+            encoding="utf-8",
+        )
+
+    def _on_load(self) -> None:
+        """从 JSON 文件加载流程数据与节点位置，重建视图。"""
+        path, _ = QFileDialog.getOpenFileName(
+            self,
+            self._tr_open_dialog_title.get(),
+            "",
+            self._tr_json_filter.get(),
+        )
+        if not path:
+            return
+        path = Path(path)
+        if not path.is_file():
+            return
+        data = json.loads(path.read_text(encoding="utf-8"))
+        node_positions = data.pop("node_positions", {})
+        flag_positions = data.pop("flag_positions", {})
+        self._node_id_to_segment_id.clear()
+        self._node_id_to_flag_id.clear()
+        for node in list(self._graph.all_nodes()):
+            self._graph.delete_nodes([node])
+        self._flow = GameScriptFlow.load_from_dict(data)
+        self._build_graph_from_flow(
+            segment_positions=node_positions,
+            flag_positions=flag_positions,
+        )
+
+    def _delete_selected_pipes(self) -> None:
+        """删除当前选中的连线。selected_pipes() 返回 (Port, Port) 列表。"""
+        for port1, port2 in self._graph.selected_pipes():
+            port1.disconnect_from(port2)

From 34961cbe72ed709ce2559088cdc8b79fdf4383c8 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 16 Feb 2026 15:23:17 +0800
Subject: [PATCH 24/40] Update .gitignore

Add ScriptFlowEditor module into ignore config.
---
 .gitignore | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 5985676..fc44314 100644
--- a/.gitignore
+++ b/.gitignore
@@ -180,9 +180,13 @@ thumbnails
 
 # version files for pyinstaller build
 versionfile_*.txt
+<<<<<<< Updated upstream
 
 run_cli.py
 run_gui.py
 
 # ScriptFlowEditor generated files
-src/ScriptFlowEditor/generated
\ No newline at end of file
+src/ScriptFlowEditor/generated
+=======
+/src/ScriptFlowEditor
+>>>>>>> Stashed changes

From 903ad25410865e6333e157595799370e2c864f53 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 16 Feb 2026 15:25:15 +0800
Subject: [PATCH 25/40] Update .gitignore

Delete 2 gui scripts.
---
 .gitignore | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/.gitignore b/.gitignore
index fc44314..1f7b077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -180,13 +180,4 @@ thumbnails
 
 # version files for pyinstaller build
 versionfile_*.txt
-<<<<<<< Updated upstream
-
-run_cli.py
-run_gui.py
-
-# ScriptFlowEditor generated files
-src/ScriptFlowEditor/generated
-=======
 /src/ScriptFlowEditor
->>>>>>> Stashed changes

From 678950b03cd7f7091c5b656a7f15697ac0dc7fb2 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 2 Mar 2026 15:43:22 +0800
Subject: [PATCH 26/40] Update fgui to renpy converter.

1. Add params for modifying main menu.
2. Change input component convert method.
---
 src/fgui_converter/FguiAssetsParseLib.py      |   2 +-
 .../utils/renpy/Fgui2RenpyConverter.py        | 247 +++++++++++++++++-
 2 files changed, 237 insertions(+), 12 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 248d12a..7257155 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -522,7 +522,7 @@ def __init__(self, display_item_tree, package_desc=None):
         # 轴心。默认值为(0.0, 0.0)。
         pivot = self.display_item_tree.get("pivot", "0.0,0.0")
         self.pivot = tuple(map(float, pivot.split(",")))
-        # 是否将轴心作为锚点。否认为False。
+        # 是否将轴心作为锚点。默认为False。
         self.pivot_is_anchor = (self.display_item_tree.get("anchor") == "true")
         # 不透明度。默认为1.0。
         alpha = self.display_item_tree.get("alpha", "1.0")
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 8be067f..831dceb 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -25,6 +25,81 @@ class BarOrientationType(IntEnum):
     HORIZONTAL = 0
     VERTICAL = 1
 
+
+class MainMenuTitle:
+    """主菜单标题：由 --main-menu-title 的键值对解析得到。"""
+    __slots__ = ('text_str', 'text_color')
+
+    def __init__(self, text_str: str | None = None, text_color: str | None = None):
+        self.text_str = text_str
+        self.text_color = text_color
+
+
+# 颜色名称到十六进制值的映射，用于 --main-menu-title text_color=名称
+COLOR_NAME_TO_HEX = {
+    'red': '#ff0000',
+    'green': '#00ff00',
+    'blue': '#0000ff',
+    'white': '#ffffff',
+    'black': '#000000',
+    'yellow': '#ffff00',
+    'cyan': '#00ffff',
+    'magenta': '#ff00ff',
+    'orange': '#ffa500',
+    'gray': '#808080',
+    'grey': '#808080',
+    'silver': '#c0c0c0',
+    'maroon': '#800000',
+    'lime': '#00ff00',
+    'olive': '#808000',
+    'navy': '#000080',
+    'purple': '#800080',
+    'teal': '#008080',
+}
+
+
+def is_valid_hex_color(color: str | None) -> bool:
+    """
+    校验颜色值是否合法：支持以下两种形式
+    1) 以 # 开头，后跟 3 位或 6 位十六进制数字，如 #f00、#ff0000；
+    2) 预定义颜色名称，如 red、green、blue（见 COLOR_NAME_TO_HEX）。
+    空字符串或 None 视为不合法。
+    """
+    if not color or not isinstance(color, str):
+        return False
+    color = color.strip()
+    if re.fullmatch(r'#[0-9A-Fa-f]{3}(?:[0-9A-Fa-f]{3})?', color):
+        return True
+    return color.lower() in COLOR_NAME_TO_HEX
+
+
+def _parse_main_menu_title_kv(pairs):
+    """
+    将 --main-menu-title key1=val1 key2=val2 解析为 MainMenuTitle。
+    pairs: 由 argparse 解析得到的字符串列表，如 ['text_str=标题', 'text_color=#ff0000']。
+    返回 MainMenuTitle 或 None（当 pairs 为空或 None 时）。
+    """
+    if not pairs:
+        return None
+    d = {}
+    for s in pairs:
+        if '=' in s:
+            k, _, v = s.partition('=')
+            d[k.strip()] = v.strip()
+    if not d:
+        return None
+    # 未提供的键为 None，赋值时保持 displayable 原值不变
+    text_str = d.get('text_str') if 'text_str' in d else d.get('text')
+    text_color = d.get('text_color') if 'text_color' in d else d.get('color')
+    if text_color is not None and not is_valid_hex_color(text_color):
+        print(f"警告: text_color 格式无效（需 # 加 3/6 位十六进制或预定义颜色名），已忽略: {text_color!r}")
+        text_color = None
+    elif text_color is not None:
+        # 颜色名解析为十六进制，便于后续使用
+        text_color = COLOR_NAME_TO_HEX.get(text_color.strip().lower(), text_color)
+    return MainMenuTitle(text_str=text_str, text_color=text_color)
+
+
 # 添加当前目录到Python路径，以便导入FguiAssetsParseLib
 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
 
@@ -76,6 +151,10 @@ class FguiToRenpyConverter:
     # 主管线提供的gallery数据文件
     ui_helper_file_name = '01_ui_helper.rpy'
 
+    # 自定义参数，用于替换Fgui发布资源中的值。
+    main_menu_title = None
+    main_menu_logo = None
+
     def __init__(self, fgui_assets):
         self.fgui_assets = fgui_assets
         self.renpy_code = []
@@ -135,11 +214,50 @@ def __init__(self, fgui_assets):
         # 一些样式预设，用于覆盖Ren'Py默认样式。
         self.style_code.append(self.style_preset)
 
-    def set_game_global_variables(self, variable_name, variable_value):
+    def set_game_global_variables(self, variable_name : str, variable_value : str | int | float | bool | None):
+        """
+        设置游戏全局变量。
+        variable_name: 变量名。
+        variable_value: 变量值。
+        """
+        if variable_value is None:
+            variable_value = 'None'
+        elif isinstance(variable_value, bool):
+            variable_value = 'True' if variable_value else 'False'
+        elif isinstance(variable_value, int):
+            variable_value = str(variable_value)
+        elif isinstance(variable_value, float):
+            variable_value = str(variable_value)
+        elif isinstance(variable_value, str):
+            variable_value = f"'{variable_value}'"
+        else:
+            variable_value = str(variable_value)
         variable_str = f"define {variable_name} = {variable_value}"
         self.game_global_variables_code.append(variable_str)
         self.game_global_variables_code.append('')
 
+    def add_game_global_variables(self, variable_name : str, variable_value : str | int | float | bool | None):
+        """
+        添加游戏全局变量。
+        variable_name: 变量名。
+        variable_value: 变量值。
+        """
+        if variable_value is None:
+            variable_value = 'None'
+        elif isinstance(variable_value, bool):
+            variable_value = 'True' if variable_value else 'False'
+        elif isinstance(variable_value, int):
+            variable_value = str(variable_value)
+        elif isinstance(variable_value, float):
+            variable_value = str(variable_value)
+        elif isinstance(variable_value, str):
+            variable_value = f"'{variable_value}'"
+        else:
+            variable_value = str(variable_value)
+        variable_str = f"default {variable_name} = {variable_value}"
+        self.game_global_variables_code.append(variable_str)
+        self.game_global_variables_code.append('')
+
     def calculate_indent(self):
         self.indent_str = self.indent_unit * self.root_indent_level
         return self.indent_str
@@ -512,6 +630,10 @@ def generate_scroll_bar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
 
         self.style_code.extend(scrollbar_style_code)
 
+    @staticmethod
+    def is_main_menu_screen(screen_name : str):
+        return screen_name == 'main_menu'
+
     @staticmethod
     def is_menu_screen(screen_name : str):
         return screen_name in FguiToRenpyConverter.menu_screen_name_list
@@ -666,7 +788,6 @@ def generate_screen(self, component : FguiComponent):
         self.screen_ui_code.clear()
         self.screen_has_dismiss = False
         self.dismiss_action_list.clear()
-        choice_screen_code = []
 
         self.screen_definition_head.append("# 界面定义")
         self.screen_definition_head.append(f"# 从FairyGUI组件{component.name}转换而来")
@@ -678,6 +799,11 @@ def generate_screen(self, component : FguiComponent):
         # 界面入参列表
         screen_params = ''
 
+        # main_menu界面的特殊处理
+        if self.is_main_menu_screen(screen_name):
+            self.generate_main_menu_screen(component)
+            return
+
         # choice界面的特殊处理
         if self.is_choice_screen(screen_name):
             self.generate_choice_screen(component)
@@ -770,6 +896,82 @@ def generate_screen(self, component : FguiComponent):
         self.screen_ui_code.append("")
         self.screen_code.extend(self.screen_ui_code)
 
+    def generate_main_menu_screen(self, component : FguiComponent):
+        print("This is main menu screen.")
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+
+        self.reset_indent_level()
+        self.screen_definition_head.append(f"screen main_menu():")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}tag menu\n")
+
+        # 自定义的组件
+        # 标题文本。若main_menu_title不为空，则先遍历一遍所有组件并修改标题文本内容。
+        if self.main_menu_title:
+            for displayable in component.display_list.displayable_list:
+                if isinstance(displayable, FguiText) and displayable.name == 'title':
+                    if self.main_menu_title.text_str:
+                        displayable.text = self.main_menu_title.text_str
+                    if self.main_menu_title.text_color:
+                        displayable.text_color = self.main_menu_title.text_color
+
+        # 游戏logo图片。若main_menu_logo不为空，则先遍历一遍所有组件并修改logo图片内容。
+        # if self.main_menu_logo:
+        #     for displayable in component.display_list.displayable_list:
+        #         if isinstance(displayable, FguiImage) and displayable.name == 'logo':
+        #             displayable.image = self.main_menu_logo
+
+        # 根据控制器列表定义界面内变量
+        if component.controller_list:
+            self.screen_variable_code.append(f"{self.indent_str}# 由组件控制器生成的界面内控制变量：") 
+        for controller in component.controller_list:
+            if not isinstance(controller, FguiController):
+                print("Component controller object type is wrong.")
+                break
+            self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}")            
+
+        # 根据组件的可见区域性质，决定是否加一层viewport。
+        if component.overflow == "hidden":
+            self.screen_ui_code.append(f"{self.indent_str}viewport:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}")
+        elif component.overflow == "scroll":
+            self.screen_ui_code.append(f"{self.indent_str}viewport:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}")
+            self.screen_ui_code.append(f"{self.indent_str}draggable True") 
+            # 在Ren'Py中实际可能无法滚动。
+            # 需要添加一个fixed组件，并设置一个合适的xysize。该xysize应为容纳所有子组件的包围框。
+            self.screen_ui_code.append(f"{self.indent_str}fixed:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize ({component.bbox_width}, {component.bbox_height})")
+
+        self.screen_ui_code.extend(self.convert_component_display_list(component))
+
+        self.screen_code.extend(self.screen_definition_head)
+        if self.screen_variable_code:
+            self.screen_code.extend(self.screen_variable_code)
+            self.screen_code.append("")
+        if self.screen_function_code:
+            self.screen_code.extend(self.screen_function_code)
+            self.screen_code.append("")
+        # 添加只有1个生效的dismiss
+        if self.screen_has_dismiss:
+            self.screen_ui_code.append(f"{self.indent_str}dismiss:")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}modal False")
+            dismiss_action_list = ', '.join(self.dismiss_action_list)
+            self.screen_ui_code.append(f"{self.indent_str}action [{dismiss_action_list}]")
+
+        self.screen_ui_code.append("")
+        self.screen_code.extend(self.screen_ui_code)
+
+
     def generate_choice_screen(self, component : FguiComponent):
         print("This is choice screen.")
         self.screen_definition_head.clear()
@@ -2102,7 +2304,6 @@ def generate_text_displayable(self, fgui_text):
             print("It is not a text displayable.")
             return text_code
 
-
         end_indent_level = 1
 
         # 根据显示控制器gearDisplay设置显示条件
@@ -2114,15 +2315,24 @@ def generate_text_displayable(self, fgui_text):
 
         # 直接定义text组件。
         # 处理换行符
-        text_str = fgui_text.text.replace("\n", "\\n").replace("\r", "\\n")
+        if fgui_text.single_line:
+            # 设置为单行，删除所有换行符
+            text_str = fgui_text.text.replace("\n", "").replace("\r", "")
+        else:
+            # 默认转义换行符
+            text_str = fgui_text.text.replace("\n", "\\n").replace("\r", "\\n")
         # 需要根据is_input区分文本组件与输入框
         if fgui_text.is_input:
             # Ren'Py中的直接使用input组件无法在多个输入框的情况下切换焦点，也无法点击空白区域让所有输入框失去焦点。
             # 需要使用button作为父组件，与InputValue关联。整个界面添加一个dismiss，空白区域点击事件让输入框失去焦点。
-            # pass
+
             # 添加InputValue变量。
-            self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name} = '{fgui_text.text}'")
-            self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name}_input_value = ScreenVariableInputValue('{fgui_text.name}', default=False)")
+            if fgui_text.custom_data:
+                self.add_game_global_variables(fgui_text.custom_data, fgui_text.text)
+                self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name}_input_value = VariableInputValue('{fgui_text.custom_data}', default=False)")
+            else:
+                self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name} = '{fgui_text.text}'")
+                self.screen_variable_code.append(f"{self.indent_str}default {fgui_text.name}_input_value = ScreenVariableInputValue('{fgui_text.name}', default=False)")
             if self.screen_has_dismiss == False:
                 self.screen_has_dismiss = True
                 # 若prompt不为空，需要在screen中添加一个输入检测函数
@@ -2227,8 +2437,8 @@ def generate_text_displayable(self, fgui_text):
         xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
         text_code.append(f"{self.indent_str}textalign {xalign}")
         # 不自动换行
-        text_code.append(f"{self.indent_str}layout 'nobreak'")
-            
+        if fgui_text.single_line:
+            text_code.append(f"{self.indent_str}layout 'nobreak'")
 
         # 粗体、斜体、下划线、删除线
         if fgui_text.bold:
@@ -2249,7 +2459,11 @@ def generate_text_displayable(self, fgui_text):
             if fgui_text.is_password:
                 text_code.append(f"{self.indent_str}mask '*'")
             # FGUI的输入限制使用正则表达式。在Ren'Py中使用字符串。此处仅为占位，无效果。
-            text_code.append(f"{self.indent_str}allow {{}}")
+            # text_code.append(f"{self.indent_str}allow {{}}")
+            if fgui_text.restrict:
+                text_code.append(f"{self.indent_str}allow '{fgui_text.restrict}'")
+            else:
+                text_code.append(f"{self.indent_str}allow {{}}")
             text_code.append(f"{self.indent_str}exclude {{}}")
         if fgui_text.is_input:
             self.indent_level_down()
@@ -2605,10 +2819,18 @@ def convert(argv):
                         help='输入FairyGUI资源文件所在目录名 (目录中需存在同名 .bytes 文件)')
     parser.add_argument('-o', '--output', type=str, 
                         help='输出Ren\'Py项目基目录路径 (即Ren\'Py项目根目录)')
-    
+    parser.add_argument('--main-menu-title', type=str, metavar='KEY=VALUE',
+                        help='主菜单标题，键值对用空格分隔写在一个字符串里，如: "text_str=标题 text_color=#ff0000"')
+    parser.add_argument('--main-menu-logo', type=str,
+                        help='主菜单logo（可选）')
     # 解析命令行参数
     args = parser.parse_args(argv[1:] if argv and len(argv) > 1 else [])
     
+    # 将 --main-menu-title 的单个字符串按空格拆成键值对列表后解析为 MainMenuTitle
+    raw = getattr(args, 'main_menu_title', None)
+    pairs = raw.strip().split() if raw else None
+    main_menu_title = _parse_main_menu_title_kv(pairs)
+    
     # 检查必需的参数
     if not args.input:
         print("错误: 必须指定输入目录 (-i 或 --input)")
@@ -2671,6 +2893,9 @@ def convert(argv):
         converter.game_dir = game_dir
         converter.scripts_dir = scripts_dir
         converter.images_dir = images_dir
+        # 设置自定义参数，用于替换Fgui发布资源中的值。
+        converter.main_menu_title = main_menu_title
+        converter.main_menu_logo = args.main_menu_logo
         print("转换器创建完成")
 
         # 生成Ren'Py代码

From 807124b9e45f6a553b450a06e88228d2ce72168b Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 3 Mar 2026 14:18:06 +0800
Subject: [PATCH 27/40] Update rounded rectangle shader

Support tuple for 4 round radius of rectangle.
---
 src/fgui_converter/FguiAssetsParseLib.py      | 14 ++++++++++--
 .../renpy/renpy_templates/02_renpy_shader.rpy | 22 ++++++++++++-------
 2 files changed, 26 insertions(+), 10 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 7257155..f91ac7e 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -646,8 +646,18 @@ def __init__(self, display_item_tree, package_desc=None):
         self.stroke_color  = hex_aarrggbb_to_rgba(stroke_color)
         fill_color = self.display_item_tree.get("fillColor", "#ffffffff") # 描边默认为白色
         self.fill_color = hex_aarrggbb_to_rgba(fill_color)
-        # 矩形可能存在圆角
-        self.corner_radius = int(self.display_item_tree.get("corner", "0"))
+        # 矩形可能存在圆角。corner 可能为逗号分隔的多个整数，转为 4 元组（不足补 0，多于 4 个舍弃）。
+        corner_raw = self.display_item_tree.get("corner")
+        if corner_raw is None:
+            self.corner_radius = (0, 0, 0, 0)
+        else:
+            segments = corner_raw.split(",")
+            # 仅当逗号分隔后真的只有一段（如 "25"）才复制为四元组；若有多段（如 "25," 得 ["25",""]）则按多段用 0 填充
+            if len(segments) == 1 and segments[0].strip():
+                self.corner_radius = (int(segments[0].strip()),) * 4
+            else:
+                parts = [int(x.strip()) if x.strip() else 0 for x in segments]
+                self.corner_radius = tuple((parts + [0] * 4)[:4])
         # 正多边形需要记录边数和顶点位置。
         # 顶点位置使用一个数组表示，数组长度等于顶点数(边数)。
         # 顶点只能存在于标准正多边形顶点到图形中心的连线上。
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
index 2256c87..c008e87 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
+++ b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
@@ -4,16 +4,19 @@ init python:
         uniform vec4 u_rectangle_color;
         uniform vec4 u_stroke_color;
         uniform vec2 u_model_size;
-        uniform float u_radius;
+        uniform vec4 u_radius;
         uniform float u_thickness;
         attribute vec2 a_tex_coord;
         varying vec2 v_tex_coord;
     """, vertex_300="""
         v_tex_coord = a_tex_coord;
     """,fragment_functions="""
-    float roundedBoxSDF(vec2 pos, vec2 border, float radius){
-        vec2 dis = abs(pos) - border + vec2(radius,radius);
-        return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - radius;
+    float roundedBoxSDF(vec2 pos, vec2 border, vec4 radius){
+        vec4 r = radius;
+        r.xy = (pos.x<0.0) ? radius.xz : radius.yw;
+        r.x = (pos.y<0.0) ? r.x : r.y;
+        vec2 dis = abs(pos) - border + vec2(r.x,r.x);
+        return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - r.x;
     }
     """,fragment_300="""
         vec2 uv = v_tex_coord - vec2(0.5, 0.5);
@@ -31,7 +34,7 @@ init python:
         uniform vec4 u_rectangle_color;
         uniform vec4 u_stroke_color;
         uniform vec2 u_model_size;
-        uniform float u_radius;
+        uniform vec4 u_radius;
         uniform float u_thickness;
         uniform float u_edge_softness;
         attribute vec2 a_tex_coord;
@@ -39,9 +42,12 @@ init python:
     """, vertex_300="""
         v_tex_coord = a_tex_coord;
     """,fragment_functions="""
-    float roundedBoxSDF(vec2 pos, vec2 border, float radius){
-        vec2 dis = abs(pos) - border + vec2(radius,radius);
-        return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - radius;
+    float roundedBoxSDF(vec2 pos, vec2 border, vec4 radius){
+        vec4 r = radius;
+        r.xy = (pos.x<0.0) ? radius.xz : radius.yw;
+        r.x = (pos.y<0.0) ? r.x : r.y;
+        vec2 dis = abs(pos) - border + vec2(r.x,r.x);
+        return length(max(dis, 0.0)) + min(max(dis.x, dis.y), 0.0) - r.x;
     }
     """,fragment_300="""
         vec2 uv = v_tex_coord - vec2(0.5, 0.5);

From ec6225e70bd07238716fed23913a4d3927a4d1c6 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Wed, 4 Mar 2026 17:49:31 +0800
Subject: [PATCH 28/40] Update list convert method.

1. fix issue of list with no default item.
2. convertor supports specific resource item.
---
 src/fgui_converter/FguiAssetsParseLib.py      | 18 ++++++++--
 .../utils/renpy/Fgui2RenpyConverter.py        | 33 +++++++++++++++----
 2 files changed, 41 insertions(+), 10 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index f91ac7e..42cd501 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -766,6 +766,14 @@ def __init__(self, list_item_tree, package_description_id=None):
         self.item_title = self.list_item_tree.get("title")
         self.item_icon = self.list_item_tree.get("icon")
         self.item_name = self.list_item_tree.get("name")
+        self.item_id = None
+        self.get_item_id_by_url()
+
+    def get_item_id_by_url(self):
+        if self.item_url and self.package_description_id:
+            self.item_id = self.item_url[self.item_url.find(self.package_description_id)+len(self.package_description_id):]
+        return
+
     def __repr__(self):
         return f"FguiListItem({self.item_url}, {self.item_title}, {self.item_icon}, {self.item_name})"
 
@@ -829,7 +837,9 @@ def __init__(self, display_item_tree, package_desc=None):
         self.item_list_length = 0
 
     def get_default_item(self, packageDescription_id):
-        self.default_item_id = self.default_item_url[self.default_item_url.find(packageDescription_id)+len(packageDescription_id):]
+        if self.default_item_url:
+            self.default_item_id = self.default_item_url[self.default_item_url.find(packageDescription_id)+len(packageDescription_id):]
+        return
 
     def set_item_list_length(self, item_list_length):
         self.item_list_length = item_list_length
@@ -882,7 +892,6 @@ def __init__(self, relation_item_tree):
         key = self.relation_item_tree.get("target")
         value = self.relation_item_tree.get("sidePair")
         self.relation_dict[key] = value
-        # print(self.relation_dict)
 
 class FguiSliderProperty:
     """
@@ -1150,7 +1159,10 @@ def clear(self):
         self.fgui_atlas_dicts.clear()
 
     def get_componentname_by_id(self, id : str) -> str:
-        return self.package_desc.id_name_mapping[id]
+        if id in self.package_desc.id_name_mapping:
+            return self.package_desc.id_name_mapping[id]
+        else:
+            return None
 
     def get_component_by_id(self, id : str) -> FguiComponent:
         for component in self.fgui_component_set:
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 831dceb..9063e73 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -2481,8 +2481,11 @@ def generate_list_displayable(self, fgui_list : FguiList) -> list[str]:
             return list_code
         component_name = self.current_component_name
 
-        # 默认引用组件可能是图片或其他组件，后续处理方式不同。
-        default_item = self.fgui_assets.get_component_by_id(fgui_list.default_item_id)
+        # 默认引用组件可能是None、图片或其他组件，后续处理方式不同。
+        if fgui_list.default_item_id:
+            default_item = self.fgui_assets.get_component_by_id(fgui_list.default_item_id)
+        else:
+            default_item = None
         default_item_type = None
         default_item_name = None
         # 若为组件
@@ -2496,7 +2499,8 @@ def generate_list_displayable(self, fgui_list : FguiList) -> list[str]:
                 default_item_type = 'image'
             else:
                 print("Ref com not found.")
-                return list_code
+                print("List default item is None.")
+                # return list_code
 
         list_length = fgui_list.get_item_list_length()
 
@@ -2508,7 +2512,8 @@ def generate_list_displayable(self, fgui_list : FguiList) -> list[str]:
         
         # 计算screen入参。
         overflow = None
-        layout = 'row'
+        # layout = 'row'
+        layout = fgui_list.layout
         row_num = 1
         column_num = 1
         transpose = False
@@ -2535,8 +2540,8 @@ def generate_list_displayable(self, fgui_list : FguiList) -> list[str]:
             row_num = fgui_list.line_item_count
             column_num = math.ceil(list_length / row_num)
         else:
-            row_num = fgui_list.line_item_count
-            column_num = fgui_list.line_item_count2
+            column_num = fgui_list.line_item_count
+            row_num = fgui_list.line_item_count2
 
         xspacing = fgui_list.col_gap
         yspacing = fgui_list.line_gap
@@ -2553,7 +2558,21 @@ def generate_list_displayable(self, fgui_list : FguiList) -> list[str]:
             # 非默认元素
             if item.item_url:
                 # TODO 非默认元素待处理
-                pass
+                # 若为组件
+                item_component = self.fgui_assets.get_component_by_id(item.item_id)
+                if item_component:# 非按钮组件可能包含多个子组件，直接引用会出现vpgrid overfull错误
+                    list_code.append(f"{self.indent_str}fixed:")
+                    self.indent_level_up()
+                    list_code.append(f"{self.indent_str}xysize {item_component.size}")
+                    list_code.append(f"{self.indent_str}use {item_component.name}()")
+                    self.indent_level_down()
+                else:
+                    image_item_name = self.fgui_assets.get_componentname_by_id(item.item_id)
+                    if image_item_name:
+                        list_code.append(f"{self.indent_str}add '{image_item_name}'")
+                    else:
+                        print(f"Image item not found: {item.item_id}")
+                # pass
             # 默认元素
             else:
                 if default_item_type == "image":

From 080ce73a9ba23375a6700c0b64da9153dfceab14 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 6 Mar 2026 23:55:25 +0800
Subject: [PATCH 29/40] Add supporting of  fgui loader with images

1. add supporting of  fgui loader with images.
2. fix bugs with global variable name.
3. fix issue of component with visible type "hidden".
---
 src/fgui_converter/FguiAssetsParseLib.py      |  23 ++-
 .../utils/renpy/Fgui2RenpyConverter.py        | 138 ++++++++++++++----
 2 files changed, 131 insertions(+), 30 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 42cd501..42d15f8 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -865,6 +865,16 @@ class FguiLoader(FguiDisplayable):
     """
     FairyGUI中的装载器。
     包含属性url：表示引用的组件url，通常是一个资源url，格式为“ui://”+“packageDescription id” + “component id”。
+    autoSize：是否自动调整尺寸。
+    Fill：填充方式。None-(无)，"scale"-等比缩放(显示全部)，"scaleNoBorder"-等比缩放(无边框)，"scaleMatchHeight"-等比缩放(适应高度)，"scaleMatchWidth"-等比缩放(适应宽度)， "scaleFree"-自由缩放。
+    shrinkOnly：是否仅允许缩小。
+    align：水平对齐方式。"left"-左对齐，"center"-居中对齐，"right"-右对齐。
+    vAlign：垂直对齐方式。"top"-上对齐，"middle"-居中对齐，"bottom"-下对齐。
+    frame：动画当前帧。若使用图片则无用处。
+    color-颜色：一个6位Hex字符串，表示显示时所有像素的RGA都要乘以该值。
+    fillMethod-填充方式："hz"-水平、"vt"-垂直、"radial90"-90度、"radial180"-180度、"radial360"-360度。
+    fillOrigin-填充原点：0(默认值)、1、2、3。该值根据不同的填充方式有不同的含义。
+    fillAmount-填充比例：100(默认值)，一个介于0到100之间的整数。
     """
     def __init__(self, display_item_tree, package_desc=None):
         if display_item_tree.tag != "loader" :
@@ -875,6 +885,16 @@ def __init__(self, display_item_tree, package_desc=None):
         if package_desc and self.url:
             self.package_description_id = package_desc.id
             self.get_item_id(package_desc.id)
+        self.auto_size = self.display_item_tree.get("autoSize", False)
+        self.fill_type = self.display_item_tree.get("fill")
+        self.shrink_only = self.display_item_tree.get("shrinkOnly", False)
+        self.align = self.display_item_tree.get("align", "left")
+        self.v_align = self.display_item_tree.get("vAlign", "top")
+        self.frame = int(self.display_item_tree.get("frame", "0"))
+        self.color = self.display_item_tree.get("color")
+        self.fill_method = self.display_item_tree.get("fillMethod")
+        self.fill_origin = self.display_item_tree.get("fillOrigin")
+        self.fill_amount = self.display_item_tree.get("fillAmount")
 
     def get_item_id(self, packageDescription_id):
         self.item_url = self.url[self.url.find(packageDescription_id)+len(packageDescription_id):]
@@ -1171,9 +1191,6 @@ def get_component_by_id(self, id : str) -> FguiComponent:
         return None
 
     def get_image_size_by_id(self, id : str):
-        # for image in self.fgui_image_set:
-        #     if image.image_id == id:
-        #         return (image.width, image.height)
         return self.package_desc.id_size_mapping[id]
 
     def __del__(self):
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 9063e73..82110ef 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -202,6 +202,7 @@ def __init__(self, fgui_assets):
         self.style_preset_template = 'renpy_style_preset_template.txt'
         self.style_preset = self.get_template_content(self.style_preset_template)
         self.list_screen_template = self.get_template_content('renpy_list_screen_template.txt')
+        self.loader_screen_template = self.get_template_content('renpy_loader_screen_template.txt')
 
         # 输出游戏目录
         self.game_dir = None
@@ -214,46 +215,26 @@ def __init__(self, fgui_assets):
         # 一些样式预设，用于覆盖Ren'Py默认样式。
         self.style_code.append(self.style_preset)
 
-    def set_game_global_variables(self, variable_name : str, variable_value : str | int | float | bool | None):
+    def set_game_global_variables(self, variable_name : str, variable_value : any):
         """
         设置游戏全局变量。
+        使用define语句修改内置变量。
         variable_name: 变量名。
         variable_value: 变量值。
         """
-        if variable_value is None:
-            variable_value = 'None'
-        elif isinstance(variable_value, bool):
-            variable_value = 'True' if variable_value else 'False'
-        elif isinstance(variable_value, int):
-            variable_value = str(variable_value)
-        elif isinstance(variable_value, float):
-            variable_value = str(variable_value)
-        elif isinstance(variable_value, str):
-            variable_value = f"'{variable_value}'"
-        else:
-            variable_value = str(variable_value)
+        variable_value = str(variable_value)
         variable_str = f"define {variable_name} = {variable_value}"
         self.game_global_variables_code.append(variable_str)
         self.game_global_variables_code.append('')
 
-    def add_game_global_variables(self, variable_name : str, variable_value : str | int | float | bool | None):
+    def add_game_global_variables(self, variable_name : str, variable_value : any):
         """
         添加游戏全局变量。
+        使用default语句添加自定义变量。
         variable_name: 变量名。
         variable_value: 变量值。
         """
-        if variable_value is None:
-            variable_value = 'None'
-        elif isinstance(variable_value, bool):
-            variable_value = 'True' if variable_value else 'False'
-        elif isinstance(variable_value, int):
-            variable_value = str(variable_value)
-        elif isinstance(variable_value, float):
-            variable_value = str(variable_value)
-        elif isinstance(variable_value, str):
-            variable_value = f"'{variable_value}'"
-        else:
-            variable_value = str(variable_value)
+        variable_value = str(variable_value)
         variable_str = f"default {variable_name} = {variable_value}"
         self.game_global_variables_code.append(variable_str)
         self.game_global_variables_code.append('')
@@ -690,6 +671,7 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                 screen_ui_code.extend(self.generate_list_displayable(displayable))
             # 装载器
             elif isinstance(displayable, FguiLoader):
+                screen_ui_code.extend(self.generate_loader_displayable(displayable))
                 pass
             # 其他组件
             else:
@@ -846,7 +828,7 @@ def generate_screen(self, component : FguiComponent):
             self.screen_ui_code.append(f"{self.indent_str}tag menu\n")
             # 若自定义了game_menu，则修改默认游戏内菜单显示的控制变量。
             if screen_name == "game_menu":
-                self.set_game_global_variables('_game_menu_screen', str("\"game_menu\""))
+                self.set_game_global_variables('_game_menu_screen', "'game_menu'")
         if self.is_modal_screen(screen_name):
             self.screen_ui_code.append(f"{self.indent_str}modal True")
             self.screen_ui_code.append(f"{self.indent_str}zorder 200\n")
@@ -865,6 +847,10 @@ def generate_screen(self, component : FguiComponent):
             self.screen_ui_code.append(f"{self.indent_str}viewport:")
             self.indent_level_up()
             self.screen_ui_code.append(f"{self.indent_str}xysize {component.size}")
+            self.screen_ui_code.append(f"{self.indent_str}draggable False")
+            self.screen_ui_code.append(f"{self.indent_str}mousewheel False")
+            self.screen_ui_code.append(f"{self.indent_str}fixed:")
+            self.indent_level_up()
         elif component.overflow == "scroll":
             self.screen_ui_code.append(f"{self.indent_str}viewport:")
             self.indent_level_up()
@@ -2613,6 +2599,104 @@ def generate_list_screen(self, fgui_list : FguiList, component_name : str) -> bo
 
         return True
 
+    def generate_loader_displayable(self, fgui_loader : FguiLoader) -> list[str]:
+        """
+        生成装载器对应的Ren'Py代码。
+        """
+        loader_code = []
+        if not isinstance(fgui_loader, FguiLoader):
+            print("It is not a loader displayable.")
+            return loader_code
+        
+        component_name = self.current_component_name
+        loader_screen_result = self.generate_loader_screen(fgui_loader, component_name)
+        if not loader_screen_result:
+            print(f"Failed to generate {component_name}_{fgui_loader.name} loader screen.")
+            return loader_code
+        
+        params_str = self.generate_loader_screen_params(fgui_loader)
+
+        loader_code.append(f"{self.indent_str}use {component_name}_{fgui_loader.name}({params_str})")
+        return loader_code
+
+    def generate_loader_screen(self, fgui_loader : FguiLoader, component_name : str) -> bool:
+        """
+        生成装载器组件的screen。于其他screen中使用use语句引用。
+        """
+        if not isinstance(fgui_loader, FguiLoader):
+            print("It is not a loader displayable.")
+            return False
+
+        # 根据模板内容替换部分字符串
+        loader_screen_template_content = self.loader_screen_template
+        loader_screen_template_content = loader_screen_template_content.replace('{component_name}', f"{component_name}")
+        loader_screen_template_content = loader_screen_template_content.replace('{loader_name}', f"{fgui_loader.name}")
+        loader_screen_template_content = loader_screen_template_content.replace('{loader_screen_name}', f"{component_name}_{fgui_loader.name}")
+        self.screen_code.append(loader_screen_template_content)
+        return True
+
+    def generate_loader_screen_params(self, fgui_loader : FguiLoader) -> str:
+        """
+        生成装载器组件screen的参数字符串。
+        """
+        if not isinstance(fgui_loader, FguiLoader):
+            print("It is not a loader displayable.")
+            return ''
+
+        child = None
+        child_name = 'Null()'
+        child_type = None
+        xysize = fgui_loader.size
+        pos = fgui_loader.xypos
+        if fgui_loader.item_url:
+            child = self.fgui_assets.package_desc.get_image_by_id(fgui_loader.item_url)
+            if child:
+                child_type = "'image'"
+                child_name = f"'{child.name}'"
+            else:
+                child = self.fgui_assets.get_component_by_id(fgui_loader.item_url)
+                if child:
+                    child_type = "'component'"
+                    child_name = child.name
+                else:
+                    child_name = 'Null()'
+        else:
+            child_name = 'Null()'
+        fit_xysize = (None, None)
+        fit_type = None
+        print(f"fgui_loader.fill_type: {fgui_loader.fill_type}")
+        if fgui_loader.fill_type:
+            if fgui_loader.fill_type == "scale":
+                fit_type = "'contain'"
+            elif fgui_loader.fill_type == "scaleNoBorder":
+                fit_type = "'cover'"
+            elif fgui_loader.fill_type == "scaleMatchHeight":
+                fit_type = "'cover'"
+                fit_xysize = (None, 1.0)
+            elif fgui_loader.fill_type == "scaleMatchWidth":
+                fit_type = "'cover'"
+                fit_xysize = (1.0, None)
+            elif fgui_loader.fill_type == "scaleFree":
+                fit_type = "'fit'"
+                fit_xysize = (1.0, 1.0)
+        if fgui_loader.shrink_only:
+            fit_type = "'scale-down'"
+            print(f"child.size: {child.size}, xysize: {xysize}")
+            if child.size[0] < xysize[0]:
+                fit_xsize = 1.0
+            else:
+                fit_xsize = None
+            if child.size[1] < xysize[1]:
+                fit_ysize = 1.0
+            else:
+                fit_ysize = None
+                fit_ysize = 1.0
+            fit_xysize = (fit_xsize, fit_ysize)
+        xalign = self.align_dict.get(fgui_loader.align, 0.0)
+        yalign = self.align_dict.get(fgui_loader.v_align, 0.0)
+
+        return f"xysize={xysize}, pos={pos}, child={child_name}, child_type={child_type}, fit_xysize={fit_xysize}, fit_type={fit_type}, child_align=({xalign},{yalign})"
+
     def generate_renpy_code(self):
         """生成完整的Ren'Py代码"""
 

From 567ff62cb1a4d3d669e911455f6a53db8520198e Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Thu, 19 Mar 2026 22:19:49 +0800
Subject: [PATCH 30/40] Add fgui-loader to renpy screen template.

1. add a renpy screen template, use for fgui loader converting.
2. add font supporting of woff and woff2 formats.
3. explicitly creating a "fixed" displayable at renpy screen code when comverting fgui-component.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  3 +
 .../utils/renpy/Fgui2RenpyConverter.py        | 80 +++++++++++++------
 .../renpy_font_map_definition.txt             |  2 +-
 .../renpy_loader_screen_template.txt          | 14 ++++
 4 files changed, 72 insertions(+), 27 deletions(-)
 create mode 100644 src/fgui_converter/utils/renpy/renpy_templates/renpy_loader_screen_template.txt

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 42d15f8..d2eab0d 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -277,6 +277,9 @@ def __init__(self, component_etree, id, name, package_desc=None):
         self.package_desc = package_desc
         size = component_etree.get("size")
         self.size = tuple(map(int, size.split(","))) if size else (0,0)
+        # 限制尺寸，4元组，分别为最小宽度、最大宽度、最小高度、最大高度。默认值为0，表示与当前实际尺寸一致。
+        restrict_size = component_etree.get("restrictSize", "0,0,0,0")
+        self.min_width, self.max_width, self.min_height, self.max_height = tuple(map(int, restrict_size.split(",")))
         self.overflow = component_etree.get("overflow", "visible")
         self.scroll = component_etree.get("scroll", "vertical")
         self.extention = component_etree.get("extention")
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 82110ef..c71438b 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -738,30 +738,7 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
 
     def generate_screen(self, component : FguiComponent):
         """
-        生成screen定义。目标样例：
-
-        screen test_main_menu():
-            add 'menu_bg':
-                pos (0, 0)
-
-            fixed:
-                pos (1007, 178)
-                use main_menu_button(title='开坑', actions=ShowMenu("save"))
-            fixed:
-                pos (1007, 239)
-                use main_menu_button(title='填坑', actions=ShowMenu("load"))
-            fixed:
-                pos (1007, 300)
-                use main_menu_button(title='设置', actions=ShowMenu("preferences"))
-            fixed:
-                pos (1007, 361)
-                use main_menu_button(title='关于', actions=ShowMenu("about"))
-            fixed:
-                pos (1007, 422)
-                use main_menu_button(title='帮助', actions=ShowMenu("help"))
-            fixed:
-                pos (1007, 483)
-                use main_menu_button(title='放弃', actions=Quit())
+        生成screen定义。
         """
         # self.screen_code.clear()
         self.screen_definition_head.clear()
@@ -842,6 +819,11 @@ def generate_screen(self, component : FguiComponent):
                 break
             self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}")            
 
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
         # 根据组件的可见区域性质，决定是否加一层viewport。
         if component.overflow == "hidden":
             self.screen_ui_code.append(f"{self.indent_str}viewport:")
@@ -905,6 +887,7 @@ def generate_main_menu_screen(self, component : FguiComponent):
                         displayable.text = self.main_menu_title.text_str
                     if self.main_menu_title.text_color:
                         displayable.text_color = self.main_menu_title.text_color
+                    break
 
         # 游戏logo图片。若main_menu_logo不为空，则先遍历一遍所有组件并修改logo图片内容。
         # if self.main_menu_logo:
@@ -921,6 +904,11 @@ def generate_main_menu_screen(self, component : FguiComponent):
                 break
             self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}")            
 
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
         # 根据组件的可见区域性质，决定是否加一层viewport。
         if component.overflow == "hidden":
             self.screen_ui_code.append(f"{self.indent_str}viewport:")
@@ -993,6 +981,11 @@ def generate_choice_screen(self, component : FguiComponent):
         choice_screen_code.append(f"# 选项界面")
         choice_screen_code.append(f"screen {component.name}({screen_params}):")
 
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
         # vbox的行距
         vbox_spacing = choice_list.line_gap if choice_list else 0
         self.reset_indent_level()
@@ -1062,6 +1055,11 @@ def generate_save_load_screen(self, component : FguiComponent):
 
         # 菜单标签
         self.screen_definition_head.append(f"{self.indent_str}tag menu")
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
         # save_slot_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=slot_list_index))
 
@@ -1251,6 +1249,10 @@ def generate_history_screen(self, component :FguiComponent):
         self.indent_level_up()
         self.screen_definition_head.append(f"{self.indent_str}tag menu")
         self.screen_definition_head.append(f"{self.indent_str}predict False")
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
         
         # history_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=history_list_index))
@@ -1335,6 +1337,11 @@ def generate_history_item(self, component : FguiComponent):
         self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
         self.indent_level_up()
 
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
         # 不包含who和what的displayList部分按顺序放在前面
         index_min = min(what_index, who_index)
         index_max = max(what_index, who_index)
@@ -1406,7 +1413,10 @@ def generate_gallery_screen(self, component : FguiComponent):
         self.screen_definition_head.append(f"screen {component.name}():")
         self.indent_level_up()
         self.screen_definition_head.append(f"{self.indent_str}tag menu")
-        self.screen_definition_head.append(f"{self.indent_str}")
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
 
         # gallery_button_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=gallery_button_list_index))
@@ -1516,7 +1526,10 @@ def generate_music_room_screen(self, component : FguiComponent):
         self.screen_definition_head.append(f"screen {component.name}():")
         self.indent_level_up()
         self.screen_definition_head.append(f"{self.indent_str}tag menu")
-        self.screen_definition_head.append(f"{self.indent_str}")
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
 
         # musicroom_button_list 之前的组件
         self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=musicroom_button_list_index))
@@ -1629,6 +1642,11 @@ def generate_say_screen(self, component : FguiComponent):
         self.screen_definition_head.append("style say_dialogue is say_what_text_style")
         self.screen_definition_head.append(f"screen {component.name}({screen_params}):")
         self.indent_level_up()
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
         self.screen_ui_code.append(f"{self.indent_str}if who is not None:")
         self.indent_level_up()
         self.screen_ui_code.append(f"{self.indent_str}add {namebox_image}:")
@@ -1707,6 +1725,16 @@ def generate_button_callable_parameter(button_title=None, actions=None, icon=Non
         parameter_str = f"{title_str}, {actions_str}, {icon_str}"
         return parameter_str
 
+    @staticmethod
+    def generate_screen_fixed_size_str(component : FguiComponent):
+        size_str = ""
+        max_width = component.max_width if component.max_width > 0 else component.size[0]
+        min_width = component.min_width if component.min_width > 0 else component.size[0]
+        max_height = component.max_height if component.max_height > 0 else component.size[1]
+        min_height = component.min_height if component.min_height > 0 else component.size[1]
+        size_str = f"maximum ({max_width}, {max_height}) minimum ({min_width}, {min_height})"
+        return size_str
+
     def generate_text_style(self, fgui_text : FguiText, style_name : str):
         """
         生成文本样式，专用于按钮标题，因为按钮中通常只有一个文本组件。
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt
index 3255979..b7c2a1c 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_font_map_definition.txt
@@ -1,7 +1,7 @@
 init python:
     import os
     font_name_list = [{font_name_list}]
-    font_file_ext = [".ttf", ".ttc", ".otf"]
+    font_file_ext = [".ttf", ".ttc", ".otf", ".woff", ".woff2"]
 
     for font_name in font_name_list:
         for font_ext in font_file_ext:
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_loader_screen_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_loader_screen_template.txt
new file mode 100644
index 0000000..25557e2
--- /dev/null
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_loader_screen_template.txt
@@ -0,0 +1,14 @@
+# {component_name} 内部装载器 {loader_name}
+screen {loader_screen_name}(xysize=(0,0), pos=(0,0), child=None, child_type="image", fit_xysize=(None,None), fit_type=None, child_align=(0.0,0.0)):
+    frame:
+        pos pos
+        xysize xysize
+        if child_type == "image":
+            add child:
+                xysize fit_xysize
+                fit fit_type
+                align child_align
+        elif child_type == "screen":
+            use expression child
+        else:
+            add null

From 6c530074a6acb9504f9de38b020d901a68e02b15 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 20 Mar 2026 18:46:49 +0800
Subject: [PATCH 31/40] Update Fgui2RenpyConverter.py

1. add transform which component was used by others.
2. some optimization of converter code.
---
 .../utils/renpy/Fgui2RenpyConverter.py        | 42 +++++++++++++------
 1 file changed, 30 insertions(+), 12 deletions(-)

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index c71438b..1691dba 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -682,15 +682,28 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     condition_str = f"showif {displayable.gear_display.controller_name} in {displayable.gear_display.controller_index}:"
                     screen_ui_code.append(f"{self.indent_str}{condition_str}")
                     self.indent_level_up()
-                    end_indent_level = 2
-                
+                    end_indent_level = end_indent_level + 1
+
+                screen_ui_code.append(f"{self.indent_str}fixed:")
+                self.indent_level_up()
+                screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                screen_ui_code.append(f"{self.indent_str}xysize {displayable.size}")
+                # 非默认锚点或轴心，以及有旋转的情况添加一个transform。
+                if displayable.pivot_is_anchor or displayable.rotation or displayable.pivot != (0.0, 0.0):
+                    screen_ui_code.append(f"{self.indent_str}at transform:")
+                    self.indent_level_up()
+                    screen_ui_code.append(f"{self.indent_str}anchor {displayable.pivot}")
+                    screen_ui_code.append(f"{self.indent_str}transform_anchor True")
+                    if displayable.rotation:
+                        screen_ui_code.append(f"{self.indent_str}rotate {displayable.rotation}")
+                    self.indent_level_down()
                 # 根据引用源id查找组件
                 ref_com = self.fgui_assets.get_component_by_id(displayable.src)
                 # 按钮。可设置标题，并根据自定义数据字段设置action。
                 if ref_com.extention == "Button" and ref_com.name != None:
-                    screen_ui_code.append(f"{self.indent_str}fixed:")
-                    self.indent_level_up()
-                    screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    # screen_ui_code.append(f"{self.indent_str}fixed:")
+                    # self.indent_level_up()
+                    # screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。
                     actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data
                     action_list = []
@@ -710,9 +723,9 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     continue
                 # 滑动条
                 if ref_com.extention == "Slider" and ref_com.name != None:
-                    screen_ui_code.append(f"{self.indent_str}fixed:")
-                    self.indent_level_up()
-                    screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                    # screen_ui_code.append(f"{self.indent_str}fixed:")
+                    # self.indent_level_up()
+                    # screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 若在自定义数据中指定了关联数据对象，则直接使用。
                     if displayable.custom_data:
                         bar_value = displayable.custom_data
@@ -728,9 +741,9 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     self.indent_level_down(end_indent_level)
                     continue
                 # 其他组件
-                screen_ui_code.append(f"{self.indent_str}fixed:")
-                self.indent_level_up()
-                screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
+                # screen_ui_code.append(f"{self.indent_str}fixed:")
+                # self.indent_level_up()
+                # screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                 screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'")
                 self.indent_level_down(end_indent_level)
 
@@ -1728,6 +1741,11 @@ def generate_button_callable_parameter(button_title=None, actions=None, icon=Non
     @staticmethod
     def generate_screen_fixed_size_str(component : FguiComponent):
         size_str = ""
+
+        if component.max_width == 0 and component.min_width == 0 and component.max_height == 0 and component.min_height == 0:
+            size_str = f"xysize {component.size}"
+            return size_str
+
         max_width = component.max_width if component.max_width > 0 else component.size[0]
         min_width = component.min_width if component.min_width > 0 else component.size[0]
         max_height = component.max_height if component.max_height > 0 else component.size[1]
@@ -2370,7 +2388,7 @@ def generate_text_displayable(self, fgui_text):
         text_code.append(f"{self.indent_str}at transform:")
         self.indent_level_up()
         text_code.append(f"{self.indent_str}anchor {fgui_text.pivot}")
-        if not fgui_text.pivot_is_anchor:
+        if fgui_text.pivot_is_anchor:
             size = fgui_text.size
             xoffset = int(fgui_text.pivot[0] * size[0])
             yoffset = int(fgui_text.pivot[1] * size[1])

From 1716f9958ac4f47719806c362d43d6078b111e2f Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sat, 21 Mar 2026 19:41:19 +0800
Subject: [PATCH 32/40] Add supporting of button click sound.

1. add supporting of button click sound.
2. fix vertical slider issue.
3. optimize screen parameters, which came from fgui-button.
---
 src/fgui_converter/FguiAssetsParseLib.py      |  48 ++++++++-
 .../utils/renpy/Fgui2RenpyConverter.py        | 102 ++++++++++++++++--
 2 files changed, 136 insertions(+), 14 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index d2eab0d..ad8ac99 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -8,19 +8,20 @@
 class FguiPackage():
     """
     FairyGUI资源包中的Package描述内容。
-    包含component列表、image列表和atlas列表。
+    包含component列表、image列表、atlas列表和音频文件列表。
     """
     id = ''
     name = ''
     component_list = []
     image_list = []
     atlas_list = []
+    sound_list = []
 
     class brief_component:
         id = ''
         name = ''
         path = ''
-        size = ()
+        size = None # tuple(width, height)
         exported = True
         def __init__(self, id, name, path, size, exported=True):
             self.id = id
@@ -34,7 +35,7 @@ class brief_image:
         id = ''
         name = ''
         path = ''
-        size = ()
+        size = None # tuple(width, height)
         scale = ''  # 九宫格：9grid；平铺：tile
         scale9grid = []
         exported = True
@@ -59,6 +60,17 @@ def __init__(self, id, size, file):
             self.size = (int(size_list[0]), int(size_list[1]))
             self.file = file
 
+    class brief_sound:
+        id = ''
+        name = ''
+        path = ''
+        file = ''
+        def __init__(self, id, name, path, file):
+            self.id = id
+            self.name = name
+            self.path = path
+            self.file = file
+
     def __init__(self, package_etree):
         self.package_etree = package_etree
         self.id = package_etree.get("id")
@@ -79,6 +91,7 @@ def __init__(self, package_etree):
                                                             exported=TransStrToBoolean(child.get('exported'))))
                 self.id_name_mapping[child.get('id')] = child.get('name')
                 self.id_size_mapping[child.get('id')] = tuple(map(int, child.get('size').split(",")))
+                continue
             # image类 name, path, size, scale=None, scale9grid=[], exported=True
             if (child.tag == 'image'):
                 self.image_list.append(self.brief_image(
@@ -91,12 +104,22 @@ def __init__(self, package_etree):
                                                     exported=TransStrToBoolean(child.get('exported'))))
                 self.id_name_mapping[child.get('id')] = child.get('name')
                 self.id_size_mapping[child.get('id')] = tuple(map(int, child.get('size').split(",")))
+                continue
             # atlas类 id, size, file
             if (child.tag == 'atlas'):
                 self.atlas_list.append(self.brief_atlas(
                                                 child.get('id'),
                                                 child.get('size'),
                                                 child.get('file')))
+                continue
+            # sound类 id, name, path, file
+            if (child.tag == 'sound'):
+                self.sound_list.append(self.brief_sound(
+                                                child.get('id'),
+                                                child.get('name'),
+                                                child.get('path'),
+                                                child.get('file')))
+                continue
 
     def clear(self):
         self.component_list.clear()
@@ -326,12 +349,22 @@ def __repr__(self):
 class FguiButton(FguiComponent):
     """
     FairyGUI中的按钮button。
-    相比其他component，多一个Button标签
+    相比其他component，多一个Button标签。
+    mode：模式。Radio-单选按钮；Check-复选按钮；None-普通按钮。
+    sound：点击音效。格式为“ui://”+“packageDescription id” + “sound id”。
+    downEffect：按下效果。scale-缩放，dark-变暗。
+    downEffectValue：按下效果值。缩放值，变暗值。
     """
     def __init__(self, component_etree, id, name, package_desc=None):
         super().__init__(component_etree, id, name, package_desc=package_desc)
         button = component_etree.find("Button")
         self.button_mode = button.get("mode")
+        sound = button.get("sound")
+        if sound:
+            # 格式为“ui://”+“packageDescription id” + “sound id”。
+            self.button_sound = sound[sound.find(package_desc.id)+len(package_desc.id):]
+        else:
+            self.button_sound = None
         self.button_down_effect = button.get("downEffect")
         self.button_down_effect_value = button.get("downEffectValue")
 
@@ -1137,7 +1170,8 @@ def __init__(self, fgui_project_path):
         self.fgui_atlas_dicts = {}
         # 组件信息
         self.fgui_component_set = []
-
+        # 音频文件信息
+        self.fgui_sound_dicts = {}
         # 先找到packageDescription，解析出component、image和atlas列表
         package_key = 'package'
         if (not self.object_dict.__contains__(package_key)):
@@ -1169,6 +1203,10 @@ def __init__(self, fgui_project_path):
             self.fgui_atlas_dicts[atlas.id] = atlas_file_name
         # 若不需要从atlas切割出单个图片，可以结合 *@sprites.bytes文件，获取每个image对象
         self.fgui_image_set = ParseFguiSpriteDescFile(self.sprite_desc_file)
+        # 根据sound_list建立sound_id与实际音频文件间的映射关系
+        for sound in self.package_desc.sound_list:
+            sound_file_name = self.fgui_project_name + '@' + sound.file
+            self.fgui_sound_dicts[sound.id] = sound_file_name
 
     def clear(self):
         self.fgui_project_name = ''
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 1691dba..74eeb5d 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -166,6 +166,7 @@ def __init__(self, fgui_assets):
         self.style_code = []
         self.image_definition_code = []
         self.graph_definition_code = []
+        self.sound_definition_code = []
         self.game_global_variables_code = []
         self.gallery_screen_code = []
         self.music_room_screen_code = []
@@ -208,6 +209,7 @@ def __init__(self, fgui_assets):
         self.game_dir = None
         self.scripts_dir = None
         self.images_dir = None
+        self.audio_dir = None
 
         # gallery相关数据
         self.gallery_data = {}
@@ -388,6 +390,16 @@ def generate_graph_definitions(self, fgui_graph : FguiGraph, component_name : st
 
         return graph_code
 
+    def generate_sound_definitions(self):
+        """生成音频定义"""
+        sound_definitions = []
+        sound_definitions.append("# 音频定义")
+        sound_definitions.append("# 根据FairyGUI发布资源中音频文件列表定义音频变量")
+        for id, file in self.fgui_assets.fgui_sound_dicts.items():
+            sound_definitions.append(f"define audio.{id} = '{file}'")
+        sound_definitions.append("")
+        self.sound_definition_code.extend(sound_definitions)
+
     def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
         """
         生成滑块样式。
@@ -452,7 +464,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
                 else:
                     print("Slider base is neither image nor graph.")
                     return
-            # FGUI中滑动条的可变bar部分
+            # FGUI中水平滑动条的可变bar部分
             if displayable.name == 'bar':
                 if isinstance(displayable, FguiImage):
                     first_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src)
@@ -463,11 +475,26 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
                     print("Slider bar is neither image nor graph.")
                     return
                 bar_id = displayable.id
+                continue
+            # FGUI中垂直滑动条的可变bar部分
+            if displayable.name == 'bar_v':
+                if isinstance(displayable, FguiImage):
+                    first_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                elif isinstance(displayable, FguiGraph):
+                    bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_slider.name))
+                    first_bar_name = f"{fgui_slider.name}_{displayable.id}"
+                else:
+                    print("Slider bar_v is neither image nor graph.")
+                    return
+                bar_id = displayable.id
+                slider_type = BarOrientationType.VERTICAL
+                continue
             # FGUI中滑动条的标题类型文本
             if displayable.name == 'title':
                 if isinstance(displayable, FguiText):
                     style_name = f"slider_{fgui_slider.id}_title"
                     self.generate_text_style(displayable, style_name)
+                    continue
                 else:
                     print("Slider title is not text.")
                     return
@@ -482,8 +509,8 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
                     # 根据grip中relation的sidePair属性来确定方向
                     side_pair_str = displayable.relations.relation_dict[bar_id]
                     # 只有该值表示垂直滑动条
-                    if side_pair_str == "bottom-bottom":
-                        slider_type = BarOrientationType.VERTICAL
+                    # if side_pair_str == "bottom-bottom":
+                    #     slider_type = BarOrientationType.VERTICAL
                 else:
                     print("Slider grp is not button.")
                     return
@@ -1707,19 +1734,45 @@ def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100,
         return barvalue_str
 
     @staticmethod
-    def generate_button_parameter(button_title=None, original_actions_str=None, icon=None):
+    def generate_button_default_parameter(button_title=None, original_actions_str=None, icon=None, activate_sound=None):
         parameter_str = ""
         title_str = "title=''"
         actions_str = "actions=NullAction()"
         icon_str = "icon=Null()"
+        activate_sound_str = "activate_sound=None"
         if button_title :
             title_str = f"title=\"{button_title}\"".replace("\n", "\\n").replace("\r", "\\n")
         if original_actions_str :
             actions_str = f"actions={original_actions_str}"
         if icon:
             icon_str = f"icon={icon}"
+        if activate_sound:
+            activate_sound_str = f"activate_sound=audio.{activate_sound}"
+        else:
+            activate_sound_str = "activate_sound=None"
+        parameter_str = f"{title_str}, {actions_str}, {icon_str}, {activate_sound_str}"
+        return parameter_str
 
-        parameter_str = f"{title_str}, {actions_str}, {icon_str}"
+    @staticmethod
+    def generate_button_parameter(button_title=None, original_actions_str=None, icon=None, activate_sound=None):
+        parameter_str = ""
+        title_str = None
+        actions_str = None
+        icon_str = None
+        activate_sound_str = None
+        if button_title :
+            title_str = f"title=\"{button_title}\"".replace("\n", "\\n").replace("\r", "\\n")
+        if original_actions_str :
+            actions_str = f"actions={original_actions_str}"
+        if icon:
+            icon_str = f"icon={icon}"
+        if activate_sound:
+            activate_sound_str = f"activate_sound=audio.{activate_sound}"
+
+        # 仅生成非None的参数
+        parameter_str = ",".join([param for param in [title_str, actions_str, icon_str, activate_sound_str] if param is not None])
+        if parameter_str:
+            parameter_str = f"{parameter_str}"
         return parameter_str
 
     @staticmethod
@@ -1996,6 +2049,8 @@ def generate_button_screen(self, component : FguiComponent):
         icon_size = (0, 0)
         icon_image = None
 
+        default_activate_sound = None
+
         for displayable in component.display_list.displayable_list:
             # 不带显示控制器表示始终显示
             if displayable.gear_display is None:
@@ -2056,11 +2111,14 @@ def generate_button_screen(self, component : FguiComponent):
         # self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name)
         self.generate_composite_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name, xysize, is_frame=is_bar_grip)
 
+        # 按钮默认点击音效
+        if component.button_sound:
+            default_activate_sound = component.button_sound
 
         # 重置缩进级别
         self.reset_indent_level()
         default_actions = component.custom_data if component.custom_data else 'NullAction()'
-        param_str = self.generate_button_parameter(default_title, default_actions, icon_image)
+        param_str = self.generate_button_default_parameter(default_title, default_actions, icon_image, default_activate_sound)
         self.screen_definition_head.append(f"screen {button_name}({param_str}):")
         # self.screen_definition_head.append(f"screen {button_name}(title='{default_title}', actions={default_actions}, icon=Null()):")
 
@@ -2102,6 +2160,8 @@ def generate_button_screen(self, component : FguiComponent):
         self.screen_ui_code.append(f"{self.indent_str}size {icon_size}")
         self.indent_level_down()
         # 一些按钮特性
+        # 点击音效
+        self.screen_ui_code.append(f"{self.indent_str}activate_sound activate_sound")
         # focus_mask，对应FGUI中的点击“测试”。
         if component.hit_test:
             # 根据引用src查找image名
@@ -2763,6 +2823,9 @@ def generate_renpy_code(self):
         # 生成图像定义
         self.generate_image_definitions()
 
+        # 生成音频定义
+        self.generate_sound_definitions()
+
         for component in self.fgui_assets.fgui_component_set:
             if component.extention == 'Button':
                 self.generate_button_screen(component)
@@ -2954,6 +3017,18 @@ def copy_atlas_files(self, source_dir, target_dir):
 
         return len(atlas_files)
 
+    def copy_sound_files(self, source_dir, target_dir):
+        """复制音频文件到目标目录"""
+        # 所有音频文件
+        sound_files = self.fgui_assets.fgui_sound_dicts.values()
+        for sound_file in sound_files:
+            source_path = os.path.join(source_dir, sound_file)
+            target_path = os.path.join(target_dir, sound_file)
+            shutil.copy2(source_path, target_path)
+            print(f"✓ 复制音频文件: {sound_file} ")
+
+        return len(sound_files)
+
 def convert(argv):
     """
     主函数：解析FguiDemoPackage并转换为Ren'Py代码
@@ -3021,16 +3096,17 @@ def convert(argv):
         game_dir = os.path.join(renpy_base_dir, "game")
         images_dir = os.path.join(game_dir, "images")
         scripts_dir = os.path.join(game_dir, "scripts")
-
+        audio_dir = os.path.join(game_dir, "audio")
         # 创建目录
         os.makedirs(game_dir, exist_ok=True)
         os.makedirs(images_dir, exist_ok=True)
         os.makedirs(scripts_dir, exist_ok=True)
+        os.makedirs(audio_dir, exist_ok=True)
         print(f"创建目录结构: {renpy_base_dir}/")
         print(f"├── game/")
         print(f"└── game/images/")
         print(f"└── game/scripts/")
-
+        print(f"└── game/audio/")
         # 创建FguiAssets对象
         print("\n正在解析FairyGUI资源...")
         fgui_assets = FguiAssets(fgui_project_path)
@@ -3042,6 +3118,7 @@ def convert(argv):
         converter.game_dir = game_dir
         converter.scripts_dir = scripts_dir
         converter.images_dir = images_dir
+        converter.audio_dir = audio_dir
         # 设置自定义参数，用于替换Fgui发布资源中的值。
         converter.main_menu_title = main_menu_title
         converter.main_menu_logo = args.main_menu_logo
@@ -3055,12 +3132,14 @@ def convert(argv):
         # 保存.rpy文件到game目录
         global_variables_output_file = os.path.join(converter.scripts_dir, "preppipe_global_variables.rpy")
         image_definition_output_file = os.path.join(converter.scripts_dir, "preppipe_image_definition.rpy")
+        sound_definition_output_file = os.path.join(converter.scripts_dir, "preppipe_sound_definition.rpy")
         style_output_file = os.path.join(converter.scripts_dir, "preppipe_styles.rpy")
         screen_output_file = os.path.join(converter.scripts_dir, "preppipe_screens.rpy")
 
         converter.save_code_to_file(global_variables_output_file, converter.game_global_variables_code)
         converter.image_definition_code.extend(converter.graph_definition_code)
         converter.save_code_to_file(image_definition_output_file, converter.image_definition_code)
+        converter.save_code_to_file(sound_definition_output_file, converter.sound_definition_code)
         converter.save_code_to_file(style_output_file, converter.style_code)
         converter.save_code_to_file(screen_output_file, converter.screen_code)
 
@@ -3073,10 +3152,15 @@ def convert(argv):
 
         # 复制图集文件到images目录
         print("\n正在复制图集文件...")
-        current_dir = os.getcwd()
+        # current_dir = os.getcwd()
         atlas_count = converter.copy_atlas_files(fgui_project_path, converter.images_dir)
         print(f"复制了 {atlas_count} 个图集文件")
 
+        # 复制音频文件到audio目录
+        print("\n正在复制音频文件...")
+        audio_count = converter.copy_sound_files(fgui_project_path, converter.audio_dir)
+        print(f"复制了 {audio_count} 个音频文件")
+
         # 一些清理
         fgui_assets.clear()
         converter.cleanup()

From fe4f98c2d98dc8c24a17ad8f224f7afb19e11862 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Fri, 27 Mar 2026 22:21:53 +0800
Subject: [PATCH 33/40] Update button controller converting method.

1. Support specific homepage index of button controller.
2. Support 6 indexed button controller template which includes 2 insensitive status.
---
 src/fgui_converter/FguiAssetsParseLib.py      | 12 ++++++---
 .../utils/renpy/Fgui2RenpyConverter.py        | 26 ++++++++++++-------
 2 files changed, 24 insertions(+), 14 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index ad8ac99..858782a 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -258,11 +258,15 @@ class FguiController:
     """
      FairyGUI控制器类
      name：控制器名称，字符串，样例"button"
-     page：索引、索引名，字符串，样例"0,up,1,down,2,over,3,selectedOver"
-     selected：初始索引号，字符串 ，样例"0"
+     homepage_type：首页类型，None或字符串。None-根据selected确定首页；"specific"-指定索引；"variable"-根据变量名查找指定索引。
+     homepage：首页索引，None或字符串，可能是数字字符串或文本字符串。
+     page：索引、索引名，字符串，样例"0,up,1,down,2,over,3,selectedOver"。
+     selected：初始索引号，字符串 ，样例"0"。
     """
-    def __init__(self, name, page, selected):
+    def __init__(self, name, homepage_type, homepage, page, selected):
         self.name = name
+        self.homepage_type = homepage_type
+        self.homepage = homepage
         self.page_index_dict = {}
         page_list = page.split(',')
         page_num = len(page_list)
@@ -330,7 +334,7 @@ def __init__(self, component_etree, id, name, package_desc=None):
         for i in range(self.child_num):
             # 控制器
             if (self.component_etree[i].tag == "controller"):
-                self.controller_list.append(FguiController(self.component_etree[i].get("name"), self.component_etree[i].get("pages"), self.component_etree[i].get("selected")))
+                self.controller_list.append(FguiController(self.component_etree[i].get("name"), self.component_etree[i].get("homePageType"), self.component_etree[i].get("homePage"), self.component_etree[i].get("pages"), self.component_etree[i].get("selected")))
             # 显示内容
             elif (self.component_etree[i].tag == "displayList"):
                 self.display_list = FguiDisplayList(self.component_etree[i], self.package_desc)
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 74eeb5d..4a02246 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -857,7 +857,11 @@ def generate_screen(self, component : FguiComponent):
             if not isinstance(controller, FguiController):
                 print("Component controller object type is wrong.")
                 break
-            self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {controller.selected}")            
+            if controller.homepage_type == "specific":
+                selected_index = int(controller.homepage)
+            else:
+                selected_index = controller.selected
+            self.screen_variable_code.append(f"{self.indent_str}default {controller.name} = {selected_index}")            
 
         # 显式添加一个fixed，设置界面尺寸。
         self.screen_ui_code.append(f"{self.indent_str}fixed:")
@@ -1889,6 +1893,7 @@ def generate_button_children(self, fgui_button : FguiButton):
         selected_hover_child_list = []
         # 非激活状体子组件列表
         insensitive_child_list = []
+        selected_insensitive_child_list = []
         # 始终显示的子组件列表
         always_show_child_list = []
         # 5种类型的子组件字典
@@ -1898,6 +1903,7 @@ def generate_button_children(self, fgui_button : FguiButton):
             'selected': selected_child_list,
             'selected_hover': selected_hover_child_list,
             'insensitive': insensitive_child_list,
+            'selected_insensitive': selected_insensitive_child_list,
             'always_show': always_show_child_list
         }
         state_index_name_dict = {
@@ -1906,6 +1912,7 @@ def generate_button_children(self, fgui_button : FguiButton):
             2: 'selected',
             3: 'selected_hover',
             4: 'insensitive',
+            5: 'selected_insensitive',
             None: 'always_show'
         }
         # 默认按钮控制器为button，并且必定有4种状态，顺序分别为idle、hover、selected、selected_hover。
@@ -1915,7 +1922,7 @@ def generate_button_children(self, fgui_button : FguiButton):
             return state_children_dict
         # 检查按钮控制器的状态列表
         state_list = fgui_button.controller_list[0].page_index_dict.keys()
-        state_number = min(len(state_list), 5) #暂时不处理5种以上的控制器状态
+        state_number = min(len(state_list), 6) #暂时不处理6种以上的控制器状态
         if state_number < 4:
             print("按钮控制器状态总数小于4。")
             return state_children_dict
@@ -1990,15 +1997,17 @@ def generate_button_screen(self, component : FguiComponent):
         selected_hover_child_list = []
         # 非激活状体子组件列表
         insensitive_child_list = []
+        selected_insensitive_child_list = []
         # 始终显示的子组件列表
         always_show_child_list = []
-        # 5种类型的子组件字典
+        # 6种类型的子组件字典
         state_children_dict = {
             'idle': idle_child_list,
             'selected': selected_child_list,
             'hover': hover_child_list,
             'selected_hover': selected_hover_child_list,
             'insensitive': insensitive_child_list,
+            'selected_insensitive': selected_insensitive_child_list,
             'always_show': always_show_child_list
         }
 
@@ -2008,6 +2017,7 @@ def generate_button_screen(self, component : FguiComponent):
             2: 'hover',
             3: 'selected_hover',
             4: 'insensitive',
+            5: 'selected_insensitive',
             None: 'always_show'
         }
         
@@ -2025,7 +2035,7 @@ def generate_button_screen(self, component : FguiComponent):
             return
         # 检查按钮控制器的状态列表
         state_list = component.controller_list[0].page_index_dict.keys()
-        state_number = min(len(state_list), 5) #暂时不处理5种以上的控制器状态
+        state_number = min(len(state_list), 6) #暂时不处理6种以上的控制器状态
         if state_number < 4:
             print("按钮控制器状态总数小于4。")
             return
@@ -2096,21 +2106,17 @@ def generate_button_screen(self, component : FguiComponent):
 
         # 根据state_children_dict生成各种对应状态的background
         is_bar_grip = self.is_bar_grip(component)
-        # self.generate_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name)
         self.generate_composite_image_object(f"{button_name}_idle_background", state_children_dict['idle'], button_name, xysize, is_frame=is_bar_grip)
         # selected_background
-        # self.generate_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name)
         self.generate_composite_image_object(f"{button_name}_selected_background", state_children_dict['selected'], button_name, xysize, is_frame=is_bar_grip)
         # hover_background
-        # self.generate_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name)
         self.generate_composite_image_object(f"{button_name}_hover_background", state_children_dict['hover'], button_name, xysize, is_frame=is_bar_grip)
         # selected_hover_background
-        # self.generate_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name)
         self.generate_composite_image_object(f"{button_name}_selected_hover_background", state_children_dict['selected_hover'], button_name, xysize, is_frame=is_bar_grip)
         # insensitive_background
-        # self.generate_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name)
         self.generate_composite_image_object(f"{button_name}_insensitive_background", state_children_dict['insensitive'], button_name, xysize, is_frame=is_bar_grip)
-
+        # selected_insensitive_background
+        self.generate_composite_image_object(f"{button_name}_selected_insensitive_background", state_children_dict['selected_insensitive'], button_name, xysize, is_frame=is_bar_grip)
         # 按钮默认点击音效
         if component.button_sound:
             default_activate_sound = component.button_sound

From bdb10968cd0bba6b50bd78d3f2c134623d8d9a31 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sun, 29 Mar 2026 21:38:46 +0800
Subject: [PATCH 34/40] Update fgui-list converting method.

1. support vertical scrollbar at left.
2. support hidden if scrollable container was not overflow.
3. update list screen template.
---
 src/fgui_converter/FguiAssetsParseLib.py      | 56 +++++++++++++++----
 .../utils/renpy/Fgui2RenpyConverter.py        | 36 +++++++-----
 .../renpy_list_screen_template.txt            | 12 ++--
 3 files changed, 72 insertions(+), 32 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 858782a..453affe 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -829,17 +829,17 @@ class FguiList(FguiDisplayable):
     scrollBar-显示滚动条：visible-可见，hidden-隐藏，auto-滚动时显示
     scrollBarRes-滚动条资源url，格式为“ui://”+“packageDescription id” + “component id”。
     scrollBarFlags：一系列滚动条标识位，可能是一个12bit整数。从低位到高位分别为：
-        0-bit：垂直滚动条显示在左边
-        1-bit：滚动位置自动贴近元件
-        2-bit：仅在内容溢出时才显示滚动条
-        3-bit：页面模式
-        4-bit 5-bit：触摸滚动效果，00默认、01启用、10关闭
-        6-bit 7-bit：边缘回弹效果，00默认、01启用、10关闭
-        8-bit：禁用惯性
-        9-bit：禁用剪裁
-        10-bit：浮动显示
-        11-bit：禁用剪裁边缘
-    当列表允许滚动时，有一大堆特性暂不处理，例如“边缘回弹”、指定滚动条组件、自动贴近元件等。
+        0-bit：垂直滚动条显示在左边，1-是，0-否。
+        1-bit：滚动位置自动贴近元件，1-是，0-否。
+        2-bit：仅在内容溢出时才显示滚动条，1-是，0-否。
+        3-bit：页面模式，1-是，0-否。
+        4-bit 5-bit：触摸滚动效果，00-默认、01-启用、10-关闭。
+        6-bit 7-bit：边缘回弹效果，00-默认、01-启用、10-关闭。
+        8-bit：禁用惯性，1-是，0-否。
+        9-bit：禁用剪裁，1-是，0-否。
+        10-bit：浮动显示，1-是，0-否。
+        11-bit：禁用剪裁边缘，1-是，0-否。
+    当列表允许滚动时，有一大堆特性暂不处理，例如“边缘回弹”、自动贴近元件等。
     margin：边缘留空，4个整数，分别对应上下左右。
     clipSoftness：边缘虚化，xy分辨对应水平与垂直方向的虚化程度。
     lineItemCount：列表布局为横向流动或分页时，表示列数。列表布局为竖向流动时，表示行数。其他布局中，该参数无效果。
@@ -857,7 +857,8 @@ def __init__(self, display_item_tree, package_desc=None):
         self.scroll = self.display_item_tree.get("scroll", "vertical")
         scrollbar_res = self.display_item_tree.get("scrollBarRes")
         self.scrollbar_res = self.get_scrollbar_res(scrollbar_res, package_desc.id)
-        self.scroll_bar_flags = int(self.display_item_tree.get("scrollBarFlags", "256"))
+        self.scroll_bar_flags = int(self.display_item_tree.get("scrollBarFlags", "0"))
+        self.scrollbar_flags_dict = self.get_scrollbar_flags_dict()
         margin = self.display_item_tree.get("margin")
         self.margin = tuple(map(int, margin.split(","))) if margin else (0,0,0,0)
         self.clip_softness = self.display_item_tree.get("clipSoftness", "0,0")
@@ -900,6 +901,37 @@ def get_scrollbar_res(self, scrollbar_res, packageDescription_id):
         else:
             return None
 
+    def get_scrollbar_flags_dict(self):
+        if self.scroll_bar_flags:
+            scrollbar_flags_list = []
+            for i in range(12):
+                scrollbar_flags_list.append((self.scroll_bar_flags & (1 << i)) >> i)
+            """
+            0-bit：垂直滚动条显示在左边，1-是，0-否。
+            1-bit：滚动位置自动贴近元件，1-是，0-否。
+            2-bit：仅在内容溢出时才显示滚动条，1-是，0-否。
+            3-bit：页面模式，1-是，0-否。
+            4-bit 5-bit：触摸滚动效果，00-默认、01-启用、10-关闭。
+            6-bit 7-bit：边缘回弹效果，00-默认、01-启用、10-关闭。
+            8-bit：禁用惯性，1-是，0-否。
+            9-bit：禁用剪裁，1-是，0-否。
+            10-bit：浮动显示，1-是，0-否。
+            11-bit：禁用剪裁边缘，1-是，0-否。
+            """
+            scrollbar_flags_dict = {
+            "display_on_left": scrollbar_flags_list[0],
+            "snapping_to_item": scrollbar_flags_list[1],
+            "hide_when_not_overflow": scrollbar_flags_list[2],
+            "page_mode": scrollbar_flags_list[3],
+            "touch_scroll": (scrollbar_flags_list[4] << 1) | (scrollbar_flags_list[5]),
+            "bounds_back": (scrollbar_flags_list[6] << 1) | (scrollbar_flags_list[7]),
+            "inertia_disabled": scrollbar_flags_list[8],
+            "mask_disabled": scrollbar_flags_list[9],
+            "floating_display": scrollbar_flags_list[10],
+            "dont_clip_margin": scrollbar_flags_list[11],
+        }
+            return scrollbar_flags_dict
+        return None
 
 class FguiLoader(FguiDisplayable):
     """
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 4a02246..60e071b 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -561,7 +561,6 @@ def generate_scroll_bar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
             base_bar Frame("vertical_scrollbar_bar")
             thumb Frame("vertical_scrollbar_grip_[prefix_]background")
         """
-        # scrollbar_image_definition_code = []
         style_definition_code = []
         scrollbar_style_code = []
         if not isinstance(fgui_scrollbar, FguiScrollBar):
@@ -728,9 +727,6 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                 ref_com = self.fgui_assets.get_component_by_id(displayable.src)
                 # 按钮。可设置标题，并根据自定义数据字段设置action。
                 if ref_com.extention == "Button" and ref_com.name != None:
-                    # screen_ui_code.append(f"{self.indent_str}fixed:")
-                    # self.indent_level_up()
-                    # screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 取FguiComponent和FguiDisplayable对象的自定义数据作为action。FguiDisplayable对象中的自定义数据优先。
                     actions = displayable.custom_data if displayable.custom_data else ref_com.custom_data
                     action_list = []
@@ -750,9 +746,6 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     continue
                 # 滑动条
                 if ref_com.extention == "Slider" and ref_com.name != None:
-                    # screen_ui_code.append(f"{self.indent_str}fixed:")
-                    # self.indent_level_up()
-                    # screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                     # 若在自定义数据中指定了关联数据对象，则直接使用。
                     if displayable.custom_data:
                         bar_value = displayable.custom_data
@@ -768,9 +761,6 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     self.indent_level_down(end_indent_level)
                     continue
                 # 其他组件
-                # screen_ui_code.append(f"{self.indent_str}fixed:")
-                # self.indent_level_up()
-                # screen_ui_code.append(f"{self.indent_str}pos {displayable.xypos}")
                 screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'")
                 self.indent_level_down(end_indent_level)
 
@@ -2701,12 +2691,32 @@ def generate_list_screen(self, fgui_list : FguiList, component_name : str) -> bo
         list_screen_template_content = list_screen_template_content.replace('{component_name}', f"{component_name}")
         list_screen_template_content = list_screen_template_content.replace('{list_name}', f"{fgui_list.name}")
         list_screen_template_content = list_screen_template_content.replace('{list_screen_name}', f"{component_name}_{fgui_list.name}")
+        side_layout = "'c r b'"
+        unscrollable_type = None
+        if fgui_list.scroll_bar_flags:
+            if fgui_list.scrollbar_flags_dict['display_on_left']:
+                side_layout = "'c l b'"
+            if fgui_list.scrollbar_flags_dict['hide_when_not_overflow']:
+                unscrollable_type = "'hide'"
+        list_screen_template_content = list_screen_template_content.replace('{side_layout}', f"{side_layout}")
         vertical_scrollbar_component, horizontal_scrollbar_component = self.get_scrollbar_style(fgui_list.scrollbar_res)
-        vscrollbar_style = vertical_scrollbar_component.name if vertical_scrollbar_component else self.default_scrollbar_style
-        hscrollbar_style = horizontal_scrollbar_component.name if horizontal_scrollbar_component else self.default_scrollbar_style
+        if vertical_scrollbar_component:
+            vscrollbar_style = vertical_scrollbar_component.name
+            vertical_scrollbar_size = (vertical_scrollbar_component.size[0], fgui_list.size[1])
+        else:
+            vscrollbar_style = self.default_scrollbar_style
+            vertical_scrollbar_size = (0, 0)
+        if horizontal_scrollbar_component:
+            hscrollbar_style = horizontal_scrollbar_component.name
+            horizontal_scrollbar_size = (fgui_list.size[0], horizontal_scrollbar_component.size[1])
+        else:
+            hscrollbar_style = self.default_scrollbar_style
+            horizontal_scrollbar_size = (0, 0)
         list_screen_template_content = list_screen_template_content.replace('{vscrollbar_style}', f"'{vscrollbar_style}'")
         list_screen_template_content = list_screen_template_content.replace('{hscrollbar_style}', f"'{hscrollbar_style}'")
-
+        list_screen_template_content = list_screen_template_content.replace('{vertical_scrollbar_size}', f"{vertical_scrollbar_size}")
+        list_screen_template_content = list_screen_template_content.replace('{horizontal_scrollbar_size}', f"{horizontal_scrollbar_size}")
+        list_screen_template_content = list_screen_template_content.replace('{unscrollable_type}', f"{unscrollable_type}")
         self.screen_code.append(list_screen_template_content)
 
         return True
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt
index 57eb36a..1f6916f 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_list_screen_template.txt
@@ -43,12 +43,10 @@ screen {list_screen_name}(overflow=None, layout='row', row_num=1, column_num=1,
                     transclude
     else:
         frame:
-            has side 'c r b'
+            has side {side_layout}  pos pos xysize xysize
             viewport id '{list_screen_name}_vp':
                 draggable True
                 mousewheel True
-                pos pos
-                xysize xysize
                 if layout == 'row':
                     hbox:
                         spacing xspacing
@@ -66,10 +64,10 @@ screen {list_screen_name}(overflow=None, layout='row', row_num=1, column_num=1,
             vbar:
                 value YScrollValue('{list_screen_name}_vp')
                 style {vscrollbar_style}
-                ysize xysize[1]
-                pos pos
+                xysize {vertical_scrollbar_size}
+                unscrollable {unscrollable_type}
             bar:
                 value XScrollValue('{list_screen_name}_vp')
                 style {hscrollbar_style}
-                xsize xysize[0]
-                pos pos
+                xysize {horizontal_scrollbar_size}
+                unscrollable {unscrollable_type}

From 2c51b621bf65dc07bb32184af78915200a586c2d Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Wed, 1 Apr 2026 00:05:30 +0800
Subject: [PATCH 35/40] Update fgui-scrollbar converting method.

Support fixed size of scrollbar thumb.
---
 src/fgui_converter/FguiAssetsParseLib.py            |  3 ++-
 .../utils/renpy/Fgui2RenpyConverter.py              | 13 +++++++++----
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index 453affe..aec6a9f 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -375,12 +375,13 @@ def __init__(self, component_etree, id, name, package_desc=None):
 class FguiScrollBar(FguiComponent):
     """
     FairyGUI中的滚动条scrollbar。
-    相比其他component，多一个ScrollBar标签，大部分情况为空。
+    相比其他component，多一个ScrollBar标签，只可能有一个属性fixedGripSize，表示滑块是否固定尺寸。
     通常滚动条都会有以个对应的其他组件，为同名带后缀“_grip”的按钮。
     """
     def __init__(self, component_etree, id, name, package_desc=None):
         super().__init__(component_etree, id, name, package_desc=package_desc)
         scrollbar = component_etree.find("ScrollBar")
+        self.fixed_grip_size = scrollbar.get("fixedGripSize")
 
 class FguiLabel(FguiComponent):
     """
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 60e071b..337ade6 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -550,7 +550,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
 
         self.style_code.extend(slider_style_code)
 
-    def generate_scroll_bar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
+    def generate_scrollbar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
         """
         生成滚动条样式。
         目标样例：
@@ -620,14 +620,19 @@ def generate_scroll_bar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
         else:
             print("Scrollbar grip is not button.")
             return
-            
+
+        # 滑块尺寸是否固定
+        thumb_size = (None, None)
+        if fgui_scrollbar.fixed_grip_size:
+            thumb_size = grip_com.size
+
         # 生成scrollbar样式
         scrollbar_style_code.append(f"style {fgui_scrollbar.name}:")
         scrollbar_style_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
         scrollbar_style_code.append(f"{self.indent_unit}bar_invert {is_vertical}")
         scrollbar_style_code.append(f"{self.indent_unit}xysize {fgui_scrollbar.size}")
         scrollbar_style_code.append(f"{self.indent_unit}base_bar Frame('{fgui_scrollbar.name}_[prefix_]bar')")
-        scrollbar_style_code.append(f"{self.indent_unit}thumb Frame('{thumb_image_name}')")
+        scrollbar_style_code.append(f"{self.indent_unit}thumb Frame('{thumb_image_name}', xysize={thumb_size})")
         scrollbar_style_code.append("")
 
         # 添加头部注释
@@ -2846,7 +2851,7 @@ def generate_renpy_code(self):
             if component.extention == 'Button':
                 self.generate_button_screen(component)
             elif component.extention == 'ScrollBar':
-                self.generate_scroll_bar_style(component)
+                self.generate_scrollbar_style(component)
             elif component.extention == 'Label':
                 pass
             elif component.extention == 'Slider':

From 9b9d4d5a63f3be0ea95b387f4124043d4fc1539b Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sat, 4 Apr 2026 20:42:42 +0800
Subject: [PATCH 36/40] Update shaders and template of rectangle and ellipse.

---
 .../renpy/renpy_templates/02_renpy_shader.rpy    | 16 ++++++++--------
 .../renpy_templates/renpy_ellipseAA_template.txt |  2 +-
 .../renpy_templates/renpy_ellipse_template.txt   |  2 +-
 .../renpy_rectangleAA_template.txt               |  2 +-
 .../renpy_templates/renpy_rectangle_template.txt |  2 +-
 5 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
index c008e87..16bd0df 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
+++ b/src/fgui_converter/utils/renpy/renpy_templates/02_renpy_shader.rpy
@@ -6,10 +6,10 @@ init python:
         uniform vec2 u_model_size;
         uniform vec4 u_radius;
         uniform float u_thickness;
-        attribute vec2 a_tex_coord;
+        attribute vec4 a_position;
         varying vec2 v_tex_coord;
     """, vertex_300="""
-        v_tex_coord = a_tex_coord;
+        v_tex_coord = a_position.xy / u_model_size;
     """,fragment_functions="""
     float roundedBoxSDF(vec2 pos, vec2 border, vec4 radius){
         vec4 r = radius;
@@ -37,10 +37,10 @@ init python:
         uniform vec4 u_radius;
         uniform float u_thickness;
         uniform float u_edge_softness;
-        attribute vec2 a_tex_coord;
+        attribute vec4 a_position;
         varying vec2 v_tex_coord;
     """, vertex_300="""
-        v_tex_coord = a_tex_coord;
+        v_tex_coord = a_position.xy / u_model_size;
     """,fragment_functions="""
     float roundedBoxSDF(vec2 pos, vec2 border, vec4 radius){
         vec4 r = radius;
@@ -66,10 +66,10 @@ init python:
         uniform vec4 u_stroke_color;
         uniform vec2 u_model_size;
         uniform float u_thickness;
-        attribute vec2 a_tex_coord;
+        attribute vec4 a_position;
         varying vec2 v_tex_coord;
     """, vertex_300="""
-        v_tex_coord = a_tex_coord;
+        v_tex_coord = a_position.xy / u_model_size;
     """,fragment_300="""
         vec2 uv = v_tex_coord - vec2(0.5, 0.5);
         float out_distance = length(uv);
@@ -88,10 +88,10 @@ init python:
         uniform vec2 u_model_size;
         uniform float u_thickness;
         uniform float u_edge_softness;
-        attribute vec2 a_tex_coord;
+        attribute vec4 a_position;
         varying vec2 v_tex_coord;
     """, vertex_300="""
-        v_tex_coord = a_tex_coord;
+        v_tex_coord = a_position.xy / u_model_size;
     """,fragment_300="""
         vec2 uv = v_tex_coord - vec2(0.5, 0.5);
         float out_distance = length(uv);
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
index 6777d81..0675ce9 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipseAA_template.txt
@@ -1,6 +1,6 @@
 # 抗锯齿的椭圆(圆形)图形定义
 image {image_name}:
-    Model().child(Solid('000')).shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01)
+    Model().shader('CursedOctopus.ellipseAA').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 0.01)
     xysize {xysize}
     pos {pos}
     anchor {anchor}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
index 25aaa92..97d82f9 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_ellipse_template.txt
@@ -1,6 +1,6 @@
 # 椭圆(圆形)图形定义
 image {image_name}:
-    Model().child(Solid('000')).shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness})
+    Model().shader('CursedOctopus.ellipse').uniform('u_ellipse_color', {ellipse_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_thickness', {stroke_thickness})
     xysize {xysize}
     pos {pos}
     anchor {anchor}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
index f795118..b456b2f 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangleAA_template.txt
@@ -1,6 +1,6 @@
 # 带光滑渐变的圆角矩形图形定义
 image {image_name}:
-    Model().child(Solid('000')).shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0)
+    Model().shader('CursedOctopus.rectangleAA').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness}).uniform('u_edge_softness', 1.0)
     xysize {xysize}
     pos {pos}
     anchor {anchor}
diff --git a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
index b58d746..4b4f874 100644
--- a/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
+++ b/src/fgui_converter/utils/renpy/renpy_templates/renpy_rectangle_template.txt
@@ -1,6 +1,6 @@
 # 圆角矩形图形定义
 image {image_name}:
-    Model().child(Solid('000')).shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness})
+    Model().shader('CursedOctopus.rectangle').uniform('u_rectangle_color', {rectangle_color}).uniform('u_stroke_color', {stroke_color}).uniform('u_model_size', {image_size}).uniform('u_radius', {round_radius}).uniform('u_thickness', {stroke_thickness})
     xysize {xysize}
     pos {pos}
     anchor {anchor}

From 2dadf2b13ad0c9cd1ffc91b8eec609449648dbac Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 7 Apr 2026 23:50:48 +0800
Subject: [PATCH 37/40] Update Fgui2RenpyConverter.py

Support scaling of fgui-slider foreground bar.
---
 .../utils/renpy/Fgui2RenpyConverter.py          | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 337ade6..01e2ee4 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -413,10 +413,10 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
             "horizontal_thumb_active"
 
         image horizontal_idle_bar_image:
-            "horizontal_bar_base"
+            Frame("horizontal_bar_base")
 
         image horizontal_hover_bar_image:
-            "horizontal_bar_active"
+            Frame("horizontal_bar_active")
 
         style horizontal_slider:
             bar_vertical False
@@ -517,9 +517,9 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
 
         # 生成bar和thumb的image
         bar_image_definition_code.append(f"image {fgui_slider.name}_base_bar_image:")
-        bar_image_definition_code.append(f"{self.indent_unit}'{second_bar_name}'")
+        bar_image_definition_code.append(f"{self.indent_unit}Frame('{second_bar_name}')")
         bar_image_definition_code.append(f"image {fgui_slider.name}_active_bar_image:")
-        bar_image_definition_code.append(f"{self.indent_unit}'{first_bar_name}'")
+        bar_image_definition_code.append(f"{self.indent_unit}Frame('{first_bar_name}')")
         bar_image_definition_code.append(f"image {fgui_slider.name}_idle_thumb_image:")
         bar_image_definition_code.append(f"{self.indent_unit}'{thumb_idle_name}'")
         bar_image_definition_code.append(f"image {fgui_slider.name}_hover_thumb_image:")
@@ -531,14 +531,15 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
         style_definition_code.append(f"style {fgui_slider.name}:")
         style_definition_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
         style_definition_code.append(f"{self.indent_unit}xysize {fgui_slider.size}")
+        style_definition_code.append(f"{self.indent_unit}bar_resizing True")
         style_definition_code.append(f"{self.indent_unit}thumb_offset {thumb_offset}")
         if is_vertical:
-            style_definition_code.append(f"{self.indent_unit}top_bar '{second_bar_name}'")
-            style_definition_code.append(f"{self.indent_unit}bottom_bar '{first_bar_name}'")
+            style_definition_code.append(f"{self.indent_unit}top_bar '{fgui_slider.name}_base_bar_image'")
+            style_definition_code.append(f"{self.indent_unit}bottom_bar '{fgui_slider.name}_active_bar_image'")
             thumb_ypos = 0
         else:
-            style_definition_code.append(f"{self.indent_unit}left_bar '{first_bar_name}'")
-            style_definition_code.append(f"{self.indent_unit}right_bar '{second_bar_name}'")
+            style_definition_code.append(f"{self.indent_unit}left_bar '{fgui_slider.name}_active_bar_image'")
+            style_definition_code.append(f"{self.indent_unit}right_bar '{fgui_slider.name}_base_bar_image'")
             thumb_xpos = 0
         style_definition_code.append(f"{self.indent_unit}thumb Fixed(Frame('{fgui_slider.name}_[prefix_]thumb_image',xysize={grip_com.size},pos=({thumb_xpos},{thumb_ypos})))")
         style_definition_code.append("")

From 2e11d6abc5eefb0d310428076daff44dd5622671 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Mon, 13 Apr 2026 21:21:42 +0800
Subject: [PATCH 38/40] Add supporting of fgui-progressbar.

---
 src/fgui_converter/FguiAssetsParseLib.py      |  25 +++-
 .../utils/renpy/Fgui2RenpyConverter.py        | 132 ++++++++++++++++--
 2 files changed, 147 insertions(+), 10 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index aec6a9f..acf243e 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -406,16 +406,17 @@ def __init__(self, component_etree, id, name, package_desc=None):
 class FguiProgressBar(FguiComponent):
     """
     FairyGUI中的进度条。
-    相比其他component，多一个ProgressBar标签，大部分情况为空。
+    被其他组件引用时，相比其他component，多一个ProgressBar标签，包含最小值、最大值与当前值。
     """
     def __init__(self, component_etree, id, name, package_desc=None):
         super().__init__(component_etree, id, name, package_desc=package_desc)
         self.progressbar = component_etree.find("ProgressBar")
+        self.title_type = self.progressbar.get("titleType")
 
 class FguiSlider(FguiComponent):
     """
     FairyGUI中的滑动条。
-    相比其他component，多一个Slider标签，大部分情况为空。
+    被其他组件引用时，相比其他component，多一个Slider标签，包含最小值、最大值与当前值。
     滑动条会有一个相应的其他组件，同名带后缀“_grip”的按钮。
     """
     def __init__(self, component_etree, id, name, package_desc=None):
@@ -595,6 +596,9 @@ def __init__(self, display_item_tree, package_desc=None):
         # Button，按钮专有属性
         self.button_property = None
 
+        # ProgressBar，进度条专有属性
+        self.progressbar_property = None
+
         # Slider，滑动条专有属性
         self.slider_property = None
 
@@ -637,6 +641,8 @@ def __init__(self, display_item_tree, package_desc=None):
                 self.relations = FguiRelation(self.display_item_tree[i])
             elif self.display_item_tree[i].tag == "Slider" :
                 self.slider_property = FguiSliderProperty(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "ProgressBar" :
+                self.progressbar_property = FguiProgressBarProperty(self.display_item_tree[i])
             else:
                 print(f"Tag not parse: {self.display_item_tree[i].tag}.")
 
@@ -986,6 +992,19 @@ def __init__(self, relation_item_tree):
         value = self.relation_item_tree.get("sidePair")
         self.relation_dict[key] = value
 
+class FguiProgressBarProperty:
+    """
+    进度条的数值属性。分别包含最小值、最大值与当前值。
+    如果某一项属性未出现则等于默认值0。
+    """
+    def __init__(self, progressbar_property_tree):
+        if progressbar_property_tree.tag != "ProgressBar":
+            raise ValueError("xml tag is not ProgressBar.")
+        self.progressbar_property_tree = progressbar_property_tree
+        self.current_value = self.progressbar_property_tree.get("value", 0)
+        self.min_value = self.progressbar_property_tree.get("min", 0)
+        self.max_value = self.progressbar_property_tree.get("max", 0)
+
 class FguiSliderProperty:
     """
     滑块的数值属性。分别包含最小值、最大值与当前值。
@@ -1231,6 +1250,8 @@ def __init__(self, fgui_project_path):
                 component = FguiLabel(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             elif extention_type == "Slider":
                 component = FguiSlider(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
+            elif extention_type == "ProgressBar":
+                component = FguiProgressBar(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             else:
                 component = FguiComponent(self.object_dict[component.id], component.id, component.name, package_desc=self.package_desc)
             self.fgui_component_set.append(component)
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 01e2ee4..1b115c2 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -400,6 +400,102 @@ def generate_sound_definitions(self):
         sound_definitions.append("")
         self.sound_definition_code.extend(sound_definitions)
 
+    def generate_progressbar_style(self, fgui_progressbar : FguiProgressBar) -> None:
+        """
+        生成进度条样式。
+        目标样例：
+        image horizontal_idle_bar_image:
+            Frame("horizontal_progressbar_base")
+
+        image horizontal_hover_bar_image:
+            Frame("horizontal_progressbar_active")
+        style horizontal_progressbar:
+            xsize 100
+            ysize 10
+            left_bar "horizontal_hover_bar_image"
+            right_bar "horizontal_idle_bar_image"
+        """
+        bar_image_definition_code = []
+        style_definition_code = []
+        progressbar_style_code = []
+        if not isinstance(fgui_progressbar, FguiProgressBar):
+            print("It is not a progressbar.")
+            return
+        
+        # 默认为水平进度条。
+        progressbar_type = BarOrientationType.HORIZONTAL
+        # bar的第一段，水平方向为right_bar。
+        first_bar_name = ''
+        # bar的第二段，水平方向为left_bar。
+        second_bar_name = ''
+
+        # progressbar 组件固定由两张图片(图形)构成。
+        # 图片(图形)固定名称为n0和bar。
+        # 其他组件暂不处理。
+        for displayable in fgui_progressbar.display_list.displayable_list:
+            # FGUI中进度条的背景
+            if displayable.name == 'n0':
+                if isinstance(displayable, FguiImage):
+                    second_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                elif isinstance(displayable, FguiGraph):
+                    bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_progressbar.name))
+                    second_bar_name = f"{fgui_progressbar.name}_{displayable.id}"
+                else:
+                    print("Slider base is neither image nor graph.")
+                    return
+            # FGUI中水平滑动条的可变bar部分
+            if displayable.name == 'bar':
+                if isinstance(displayable, FguiImage):
+                    first_bar_name = self.fgui_assets.get_componentname_by_id(displayable.src)
+                elif isinstance(displayable, FguiGraph):
+                    bar_image_definition_code.extend(self.generate_graph_definitions(displayable, fgui_progressbar.name))
+                    first_bar_name = f"{fgui_progressbar.name}_{displayable.id}"
+                else:
+                    print("Slider bar is neither image nor graph.")
+                    return
+                bar_id = displayable.id
+                continue
+            # FGUI中滑动条的标题类型文本
+            if displayable.name == 'title':
+                if isinstance(displayable, FguiText):
+                    style_name = f"slider_{fgui_progressbar.id}_title"
+                    self.generate_text_style(displayable, style_name)
+                    # 暂时不生成标题文本
+                    continue
+                else:
+                    print("ProgressBar title is not text.")
+                    return
+
+        # 生成bar的image
+        bar_image_definition_code.append(f"image {fgui_progressbar.name}_base_bar_image:")
+        bar_image_definition_code.append(f"{self.indent_unit}Frame('{second_bar_name}')")
+        bar_image_definition_code.append(f"image {fgui_progressbar.name}_active_bar_image:")
+        bar_image_definition_code.append(f"{self.indent_unit}Frame('{first_bar_name}')")
+        bar_image_definition_code.append("")
+
+        # 生成progressbar样式
+        is_vertical = (progressbar_type==BarOrientationType.VERTICAL)
+        style_definition_code.append(f"style {fgui_progressbar.name}:")
+        style_definition_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
+        style_definition_code.append(f"{self.indent_unit}xysize {fgui_progressbar.size}")
+        # FairyGUI中前景图片使用缩放来改变进度，Ren'Py中使用bar_resizing来实现。
+        style_definition_code.append(f"{self.indent_unit}bar_resizing True")
+        if is_vertical:
+            style_definition_code.append(f"{self.indent_unit}top_bar '{fgui_progressbar.name}_base_bar_image'")
+            style_definition_code.append(f"{self.indent_unit}bottom_bar '{fgui_progressbar.name}_active_bar_image'")
+            thumb_ypos = 0
+        else:
+            style_definition_code.append(f"{self.indent_unit}left_bar '{fgui_progressbar.name}_active_bar_image'")
+            style_definition_code.append(f"{self.indent_unit}right_bar '{fgui_progressbar.name}_base_bar_image'")
+        style_definition_code.append("")
+
+        # 添加头部注释
+        progressbar_style_code.extend(bar_image_definition_code)
+        progressbar_style_code.append("# 进度条样式定义")
+        progressbar_style_code.extend(style_definition_code)
+
+        self.style_code.extend(progressbar_style_code)
+
     def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
         """
         生成滑块样式。
@@ -425,6 +521,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
             thumb_offset 24
             left_bar "horizontal_hover_bar_image"
             right_bar "horizontal_idle_bar_image"
+            bar_resizing True
             thumb Fixed(Frame("horizontal_[prefix_]thumb_image",xsize=48,ysize=54,ypos=-22))
         """
         bar_image_definition_code = []
@@ -494,6 +591,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
                 if isinstance(displayable, FguiText):
                     style_name = f"slider_{fgui_slider.id}_title"
                     self.generate_text_style(displayable, style_name)
+                    # 暂时不生成标题文本
                     continue
                 else:
                     print("Slider title is not text.")
@@ -531,6 +629,7 @@ def generate_slider_style(self, fgui_slider : FguiSlider) -> None:
         style_definition_code.append(f"style {fgui_slider.name}:")
         style_definition_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
         style_definition_code.append(f"{self.indent_unit}xysize {fgui_slider.size}")
+        # FairyGUI中前景图片使用缩放来改变进度，Ren'Py中使用bar_resizing来实现。
         style_definition_code.append(f"{self.indent_unit}bar_resizing True")
         style_definition_code.append(f"{self.indent_unit}thumb_offset {thumb_offset}")
         if is_vertical:
@@ -628,13 +727,13 @@ def generate_scrollbar_style(self, fgui_scrollbar : FguiScrollBar) -> None:
             thumb_size = grip_com.size
 
         # 生成scrollbar样式
-        scrollbar_style_code.append(f"style {fgui_scrollbar.name}:")
-        scrollbar_style_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
-        scrollbar_style_code.append(f"{self.indent_unit}bar_invert {is_vertical}")
-        scrollbar_style_code.append(f"{self.indent_unit}xysize {fgui_scrollbar.size}")
-        scrollbar_style_code.append(f"{self.indent_unit}base_bar Frame('{fgui_scrollbar.name}_[prefix_]bar')")
-        scrollbar_style_code.append(f"{self.indent_unit}thumb Frame('{thumb_image_name}', xysize={thumb_size})")
-        scrollbar_style_code.append("")
+        style_definition_code.append(f"style {fgui_scrollbar.name}:")
+        style_definition_code.append(f"{self.indent_unit}bar_vertical {is_vertical}")
+        style_definition_code.append(f"{self.indent_unit}bar_invert {is_vertical}")
+        style_definition_code.append(f"{self.indent_unit}xysize {fgui_scrollbar.size}")
+        style_definition_code.append(f"{self.indent_unit}base_bar Frame('{fgui_scrollbar.name}_[prefix_]bar')")
+        style_definition_code.append(f"{self.indent_unit}thumb Frame('{thumb_image_name}', xysize={thumb_size})")
+        style_definition_code.append("")
 
         # 添加头部注释
         # scrollbar_style_code.extend(scrollbar_image_definition_code)
@@ -704,6 +803,7 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
             # 装载器
             elif isinstance(displayable, FguiLoader):
                 screen_ui_code.extend(self.generate_loader_displayable(displayable))
+                # 目前装载器只支持图片。
                 pass
             # 其他组件
             else:
@@ -766,6 +866,22 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'")
                     self.indent_level_down(end_indent_level)
                     continue
+                # 进度条
+                if ref_com.extention == "ProgressBar" and ref_com.name != None:
+                    # 若在自定义数据中指定了关联数据对象，则直接使用。
+                    if displayable.custom_data:
+                        bar_value = displayable.custom_data
+                    # 否则再查找引用源对象的自定义数据
+                    elif ref_com.custom_data:
+                        bar_value = ref_com.custom_data
+                    # 若未指定则在screen中生成一个临时变量
+                    else:
+                        variable_name = f"{component.name}_{displayable.name}_barvalue"
+                        screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.progressbar_property.current_value)}")
+                        bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.progressbar_property.min_value, max_value=displayable.progressbar_property.max_value)
+                    screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'")
+                    self.indent_level_down(end_indent_level)
+                    continue
                 # 其他组件
                 screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'")
                 self.indent_level_down(end_indent_level)
@@ -2860,7 +2976,7 @@ def generate_renpy_code(self):
             elif component.extention == 'ComboBox':
                 pass
             elif component.extention == 'ProgressBar':
-                pass
+                self.generate_progressbar_style(component)
             else:
                 self.generate_screen(component)
         

From 9a1f98bd5b19c3f840f16a16f3998a25573474e5 Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Tue, 14 Apr 2026 17:43:22 +0800
Subject: [PATCH 39/40] Update Fgui2RenpyConverter.py

Change progress bar related variable type to ui.adjustment, made bar unadjustable.
---
 .../utils/renpy/Fgui2RenpyConverter.py          | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index 1b115c2..ac7ba3b 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -870,16 +870,16 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                 if ref_com.extention == "ProgressBar" and ref_com.name != None:
                     # 若在自定义数据中指定了关联数据对象，则直接使用。
                     if displayable.custom_data:
-                        bar_value = displayable.custom_data
+                        variable_name = displayable.custom_data
                     # 否则再查找引用源对象的自定义数据
                     elif ref_com.custom_data:
-                        bar_value = ref_com.custom_data
+                        variable_name = ref_com.custom_data
                     # 若未指定则在screen中生成一个临时变量
                     else:
-                        variable_name = f"{component.name}_{displayable.name}_barvalue"
-                        screen_ui_code.append(f"{self.indent_str}{self.generate_variable_definition_str(variable_name, current_value=displayable.progressbar_property.current_value)}")
-                        bar_value = self.generate_barvalue_definition_str(variable_name, min_value=displayable.progressbar_property.min_value, max_value=displayable.progressbar_property.max_value)
-                    screen_ui_code.append(f"{self.indent_str}bar value {bar_value} style '{ref_com.name}' id '{component.name}_{displayable.id}'")
+                        variable_name = f"{component.name}_{displayable.name}_value"
+                        range_value = (int)(displayable.progressbar_property.max_value) - (int)(displayable.progressbar_property.min_value)
+                        screen_ui_code.append(f"{self.indent_str}{self.generate_uiadjustment_definition_str(name=variable_name, range=range_value, value=(int)(displayable.progressbar_property.current_value))}")
+                    screen_ui_code.append(f"{self.indent_str}bar adjustment {variable_name} style '{ref_com.name}' id '{component.name}_{displayable.id}'")
                     self.indent_level_down(end_indent_level)
                     continue
                 # 其他组件
@@ -1840,6 +1840,11 @@ def get_image_name(self, fgui_image):
     def generate_variable_definition_str(variable_name, current_value=None):
         return f"default {variable_name} = {current_value}"
 
+    @staticmethod
+    def generate_uiadjustment_definition_str(name, range=1, value=0):
+        # return f"default {uiadjustment_name} = {adjustment_value}"
+        return f"default {name} = ui.adjustment(range={range}, value={value}, adjustable=False)"
+
     @staticmethod
     def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100, current_value=0, scope='local'):
         barvalue_str = ''

From 17fc1efba8aa5a5d4f8be3a82653678871e6b43d Mon Sep 17 00:00:00 2001
From: CursedOctopus <cursedoctopus@gmail.com>
Date: Sat, 18 Apr 2026 19:28:18 +0800
Subject: [PATCH 40/40] Add supporting of fgui-lable.

---
 src/fgui_converter/FguiAssetsParseLib.py      |  14 ++
 .../utils/renpy/Fgui2RenpyConverter.py        | 121 ++++++++++++++++--
 2 files changed, 127 insertions(+), 8 deletions(-)

diff --git a/src/fgui_converter/FguiAssetsParseLib.py b/src/fgui_converter/FguiAssetsParseLib.py
index acf243e..cf41658 100644
--- a/src/fgui_converter/FguiAssetsParseLib.py
+++ b/src/fgui_converter/FguiAssetsParseLib.py
@@ -643,6 +643,8 @@ def __init__(self, display_item_tree, package_desc=None):
                 self.slider_property = FguiSliderProperty(self.display_item_tree[i])
             elif self.display_item_tree[i].tag == "ProgressBar" :
                 self.progressbar_property = FguiProgressBarProperty(self.display_item_tree[i])
+            elif self.display_item_tree[i].tag == "Label" :
+                self.label_property = FguiLabelProperty(self.display_item_tree[i])
             else:
                 print(f"Tag not parse: {self.display_item_tree[i].tag}.")
 
@@ -678,6 +680,18 @@ def __init__(self, component_property_tree):
         self.controller_index = self.component_property_tree.get("page")
         # TODO 待添加点击音效
 
+class FguiLabelProperty(FguiComponentPropertyBase):
+    """
+    组件子属性中的Label信息。
+    """
+    def __init__(self, component_property_tree):
+        super().__init__(component_property_tree)
+        if self.property_name != "Label" :
+            raise ValueError("xml tag is not Label")
+        self.title = self.component_property_tree.get("title")
+        self.text_color = self.component_property_tree.get("titleColor")
+        self.font_size = int(self.component_property_tree.get("titleFontSize", 24))
+
 class FguiGraph(FguiDisplayable):
     """
     FairyGUI中的图形。包括空白、矩形(圆边矩形)、圆形(椭圆)、多边形等。
diff --git a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
index ac7ba3b..191b702 100644
--- a/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
+++ b/src/fgui_converter/utils/renpy/Fgui2RenpyConverter.py
@@ -883,11 +883,102 @@ def convert_component_display_list(self, component: FguiComponent, list_begin_in
                     self.indent_level_down(end_indent_level)
                     continue
                 # 其他组件
-                screen_ui_code.append(f"{self.indent_str}use {ref_com.name} id '{component.name}_{displayable.id}'")
+                parameter_str=''
+                parameter_str = self.generate_label_screen_parameter(displayable.label_property)
+
+
+                screen_ui_code.append(f"{self.indent_str}use {ref_com.name}({parameter_str}) id '{component.name}_{displayable.id}'")
                 self.indent_level_down(end_indent_level)
 
         return screen_ui_code
 
+    def generate_label_screen(self, component : FguiComponent):
+        """
+        生成FGUI标签组件对应screen。目标样例：
+        screen label_screen(title='', xysize=(100, 100)):
+            fixed:
+                xysize xysize
+                text title:
+                    align (0.5, 0.5)
+                    font "hyzjhj.ttf"
+                    color "#FEDAAA"
+                    size 32
+        """
+        self.screen_definition_head.clear()
+        self.screen_variable_code.clear()
+        self.screen_function_code.clear()
+        self.screen_ui_code.clear()
+        self.screen_has_dismiss = False
+        self.dismiss_action_list.clear()
+        self.reset_indent_level()
+ 
+        # 生成标签组件screen
+        self.screen_definition_head.append("# 标签screen定义")
+        self.screen_definition_head.append(f"# 从FairyGUI标签组件{component.name}转换而来")
+        id = component.id
+        label_name = component.name
+        xysize = component.size
+
+        title_index = -1
+        default_title = ''
+        title_displayable = None
+        text_xalign, text_yalign = 0, 0
+        title_pos = (0, 0)
+        title_anchor = (0, 0)
+        title_xysize = (0, 0)
+        title_style = None
+        title_color = '#FFFFFF'
+        title_size = 32
+
+        # 查找title文本组件
+        for index, displayable in enumerate(component.display_list.displayable_list):
+            if isinstance(displayable, FguiText) and displayable.name == 'title':
+                title_index = index
+                title_displayable = displayable
+                default_title = title_displayable.text
+                if displayable.single_line:
+                    default_title = default_title.replace("\n", "").replace("\r", "")
+                title_xysize = title_displayable.size
+                text_xalign, text_yalign = self.trans_text_align(title_displayable.align, title_displayable.v_align)
+                title_pos = title_displayable.xypos
+                title_anchor = title_displayable.pivot
+                self.generate_text_style(title_displayable, f"{label_name}_title_text")
+                title_style = f"{label_name}_title_text"
+                title_color = title_displayable.text_color
+                title_size = title_displayable.font_size
+                break
+
+        # 界面定义
+        self.screen_definition_head.append(f"screen {label_name}(title_text='{default_title}', title_color='{title_color}', title_size={title_size}):")
+        self.indent_level_up()
+
+        # 显式添加一个fixed，设置界面尺寸。
+        self.screen_ui_code.append(f"{self.indent_str}fixed:")
+        self.indent_level_up()
+        self.screen_ui_code.append(f"{self.indent_str}{self.generate_screen_fixed_size_str(component)}")
+
+        # 生成title组件前的组件
+        self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=0, list_end_index=title_index))
+
+        if title_displayable:
+            # title组件
+            self.screen_ui_code.append(f"{self.indent_str}text title_text style '{title_style}':")
+            self.indent_level_up()
+            self.screen_ui_code.append(f"{self.indent_str}xysize {title_xysize}")
+            self.screen_ui_code.append(f"{self.indent_str}color title_color")
+            self.screen_ui_code.append(f"{self.indent_str}size title_size")
+
+            self.indent_level_down()
+
+            # 生成title组件后的组件
+            self.screen_ui_code.extend(self.convert_component_display_list(component, list_begin_index=title_index + 1, list_end_index=-1))
+
+        self.screen_code.extend(self.screen_definition_head)
+
+        self.screen_ui_code.append("")
+        self.screen_code.extend(self.screen_ui_code)
+
+
     def generate_screen(self, component : FguiComponent):
         """
         生成screen定义。
@@ -1854,6 +1945,19 @@ def generate_barvalue_definition_str(barvalue_name, min_value=0, max_value=100,
         barvalue_str = f"{barvalue_scope_str}VariableValue('{barvalue_name}',min={min_value},max={max_value})"
         return barvalue_str
 
+    @staticmethod
+    def generate_label_screen_parameter(label_property):
+        parameter_list = []
+        parameter_str = ""
+        if label_property.title:
+            parameter_list.append(f"title_text='{label_property.title}'")
+        if label_property.text_color:
+            parameter_list.append(f"title_color='{label_property.text_color}'")
+        if label_property.font_size:
+            parameter_list.append(f"title_size={label_property.font_size}")
+        parameter_str = ",".join(parameter_list)
+        return parameter_str
+
     @staticmethod
     def generate_button_default_parameter(button_title=None, original_actions_str=None, icon=None, activate_sound=None):
         parameter_str = ""
@@ -1962,8 +2066,6 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str):
         self.style_code.append(f"{style_indent}font '{text_font}'")
         self.style_code.append(f"{style_indent}size {fgui_text.font_size}")
         self.style_code.append(f"{style_indent}color '{fgui_text.text_color}'")
-        # xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
-        # self.style_code.append(f"{style_indent}align ({xalign}, {yalign})")
 
         text_outline_string = self.generate_text_outline_string(fgui_text)
         self.style_code.append(f"{style_indent}outlines {text_outline_string}")
@@ -1972,8 +2074,10 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str):
         #设置最小宽度才能使text_align生效
         self.style_code.append(f"{style_indent}min_width {fgui_text.size[0]}")
         xalign, yalign = self.trans_text_align(fgui_text.align, fgui_text.v_align)
-        # 默认两侧居中对齐
+        # 默认文本居中，如果文本长度超出文本框，效果可能与FairyGUI不一致。
         self.style_code.append(f"{style_indent}textalign {xalign}")
+        # 垂直方向对齐方式使用yalign，可能有问题。
+        self.style_code.append(f"{style_indent}yalign {yalign}")
         # 粗体、斜体、下划线、删除线
         if fgui_text.bold:
             self.style_code.append(f"{style_indent}bold {fgui_text.bold}")
@@ -1982,7 +2086,9 @@ def generate_text_style(self, fgui_text : FguiText, style_name : str):
         if fgui_text.underline:
             self.style_code.append(f"{style_indent}underline {fgui_text.underline}")
         if fgui_text.strike:
-            self.style_code.append(f"{style_indent}strikethrough {fgui_text.strike}")
+            self.style_code.append(f"{style_indent}strikethrough {fgui_text.strike}")        # 不自动换行
+        if fgui_text.single_line:
+            self.style_code.append(f"{style_indent}layout 'nobreak'")
         self.style_code.append("")
 
     @staticmethod
@@ -2099,7 +2205,6 @@ def generate_button_screen(self, component : FguiComponent):
             textalign 0.5
         """
 
-        # self.screen_code.clear()
         self.screen_definition_head.clear()
         self.screen_variable_code.clear()
         self.screen_function_code.clear()
@@ -2295,7 +2400,6 @@ def generate_button_screen(self, component : FguiComponent):
 
         self.screen_code.extend(self.screen_definition_head)
         self.screen_code.extend(self.screen_ui_code)
-        # self.renpy_code.extend(self.screen_code)
 
     def trans_text_align(self, text_horizontal_align="left", text_vertical_align="top"):
         return self.align_dict.get(text_horizontal_align, 0.5), self.align_dict.get(text_vertical_align, 0.5)
@@ -2975,11 +3079,12 @@ def generate_renpy_code(self):
             elif component.extention == 'ScrollBar':
                 self.generate_scrollbar_style(component)
             elif component.extention == 'Label':
-                pass
+                self.generate_label_screen(component)
             elif component.extention == 'Slider':
                 self.generate_slider_style(component)
             elif component.extention == 'ComboBox':
                 pass
+                # self.generate_combobox_screen(component)
             elif component.extention == 'ProgressBar':
                 self.generate_progressbar_style(component)
             else:
