界面

本页面讲述的内容长期有效
咯咯炀讨论 | 贡献2020年11月11日 (三) 19:26的版本 (咯咯炀移动页面Interface界面

CK3's user interface (UI) is highly moddable, but for this reason UI mods disable achievements, since a player could cheat with them.

The game also includes a GUI editor, which allows to inspect UI elements and edit them in the game.


Modders can:

  • change the visual style of the interface
  • make windows movable and resizable
  • change and remove elements
  • add new buttons
  • show more information from the code
  • add new windows (with a workaround)

Modders can’t:

  • add new hotkeys (only reuse the existing ones)
  • display information from one window in another, unless developers included that possibility.


Basics

The interface in CK3 is created through .gui files in the game/gui folder, which are somewhat similar to html files.

As such, you can edit them with any text editor, like VS Code, Sublime or Atom. Choose Python or Perl 6 for syntax highlighting, they fit well.

CK3 uses .dds files for textures, which are saved inside the game/gfx/ folder.

To edit or save .dds files use either Photoshop with Intel plugin or GIMP with this plugin.


To reload gui files in the game and use the GUI editor, add -debug_mode and -develop launch options:

  • right-click the game on Steam, choose Properties, Set Launch Options, add -debug_mode -develop

You can use the following console commands:

  • reload gui - reloads all gui files to display any changes in the game
  • reload texture - reloads all texture files
  • gui editor - opens the GUI editor
  • tweak gui.debug - allows to enable highlighting of UI elements. The tooltips will display their name, size and position.
  • DumpDataTypes - will create a data_types.log in your log folder (Documents/Paradox Interactive/Crusader Kings III/logs) listing all available GUI functions for each window/game object

Other tips:

  • always have the error log open to see if there is a mistake in the code (it's in the same log folder)
  • enable the error tracker in the game by clicking "Toggle Release Mode" below the console, to see if your changes craeted any new errors
  • mute the game when using reload gui command, as it triggers the intro sound every time
  • use Reload GUI mod that adds a button to reload gui (with a hotkey) and closes the Settings window when you reload
  • add the gui folder to your text editor, so it uses it for autocompletion
  • you can use test_gui.gui in gui/debug for testing. To show this window, open the console and click Test Window.
  • fold code so it's easier to see its structure. Usual hotkeys are Ctrl+K, Ctrl+1 (where 1 is the level at which to fold the code).

Creating a GUI mod

1. Start the game launcher, go to Mods, Mod Tools and fill in all the fields, including tags.

Clicking Create will create a new folder and a .mod file in Documents/Paradox Interactive/Crusader Kings III/mod.

2. Next, create a "gui" folder inside your mod and copy the files you want to mod there from game/gui.

  • If you don’t know which file is needed, use the GUI editor and inspect it in the game.

GUI Editor

GUI Editor is a developer tool for editing the UI in the game.

To use it you need to launch the game with -debug_mode and -developer options.


To open the editor, either:

  • press Ctrl+F8
  • open the console with the ` key (below Esc), click GUI Editor
  • open the console, run gui_editor command

Features

By default the editor starts with the Edit mode enabled. You can disable it in the top window, called Outliner. Hotkey "E".

  • The edit mode is similar to the Inspect mode in browsers. While it’s enabled you can’t interact with the game, but it allows you to select parts of the UI and change them in the Properties window below.
  • Scroll with your mouse wheel to change what element the editor should focus on, since hud.gui tends to get on top of other windows.
  • Yellow border indicated the selected element. To hide other borders, uncheck "Show Hierarchy" in the Outliner (Hotkey "L").
  • Holding the right mouse button allows to move the selected element.
  • To undo anything, press Ctrl+Z or the undo button in the Outliner. Redo is next to it, Ctrl+Y.
  • Red stars * in the Outliner indicate unsaved changes. Press Ctrl+S or the save button at the top to save them. Make sure the gui files you're editing are in your mod, otherwise it will write the changes to the game folder. (To reset them, verify integrity from Steam's properties window)
  • You can move any dev windows by dragging them and resize them by dragging the edges.
  • You can drag UI elements in the Outliner's hierarchy to reorder them. Right-click to show the context menu.
  • You can change or add new properties (by clicking the plus symbol) in the Properties window.


By clicking "Window" in the Outliner, you can open two more windows: UI components and Registered Data Types.

  • UI Components is like a palette from which you can drag new elements to the UI. gui/shared/standard.gui and gui/defaults.gui contain the most common things, like buttons, icons and text.
  • Registered Data Types may be used to look up what functions are available to display data from the game.
  • Clicking the top right button in the Data Types will dump this data to your log folder (Documents/Paradox Interactive/CK3/logs). You can also use "DumpDataTypes" console command.


Note: it is often easy to select a template by mistake, changing which will affect all instances of it in the UI. Pay attention to what file you have selected in the outliner. If you see in the Properties window a blue header that starts with "type:", it is a template, so be careful not to edit this part (unless you intend to).

UI code

CK3’s UI is composed of containers and objects inside them.

Most windows, for example, are created using a window container, while map icons use a widget or an hbox.

The order in the file determines the order on the screen: what's lower in the code will appear on a higher layer.

Most elements can also contain others, for example, there can be a textbox inside an icon inside a button. Nested elemenets, aka children, will will be moved with their parent.

Position is set relative to the top left corner (either of the screen or the parent). This can be changed with parentanchor property. The available options are: left, right, top, bottom, hcenter (horizontal center) and vcenter (vertical center). They can be combined with a | like this: parentanchor = right|vcenter.

Every element is opened and closed with curly brackets, like this: container = { }.

The common code style is to open and close the block on the same level, while indenting the contents with one tab:

widget = {
  size = { 50 50 }
  alpha = 0.5
}

This helps you see the structure of the code better, notice any missing or extra brackets, and is needed for some editors to correctly fold the blocks of code.

UI Components

Window

  • The only movable container. To enable movement, add movable = yes property.
  • Can be fixed size or resized by its children.
  • In the game the background is set using templates, like using = Window_Background and using = Window_Decoration.
  • If a child is outside of a window, it won't be clickable and won't show a tooltip. Use allow_outside = yes to change this.

widget

  • A static container. Similar to a window in other regards.

margin_widget

  • Similar to a widget, but can be resized with margins. (This allows us to make windows that resize to screens of different size, by setting height to 100% and margins to ~50 to show the hud)

container

  • Does not have a fixed size (but you can set maximumsize).
  • Resizes automatically to fit all its children, including invisible ones. Use ignoreinvisible = yes to ignore them.
  • Often used to group multiple elements to move them together.

flowcontainer

  • Arranges all its children in a horizontal row. Use direction = vertical to make it vertical.
  • Doesn't ignore invisible children by default. Use ignoreinvisible = yes to change it.
  • Does not have a fixed size.
  • Its children cannot have positions, as they are set automatically.
  • If you need to adjust position of its child, you can put it inside a container or a widget and then change position relative to this parent.

hbox vbox

  • Arranges all its children in a horizontal row and spreads them along its width. Vbox is the same but vertical.
  • Cannot have a fixed size, but instead takes the width of its parent as its own (ignoring the parent's margins). Vbox takes the height.
    • If its parent can't have a fixed size (like flowcontainer), this may crash the game.
  • Can be limited by minimumsize/maximumsize and margins.
  • Ignores invisible children by default. Use ignoreinvisible = no to change this.
  • An hbox inside another hbox will have 0 size and won't spread its children. Use layoutpolicy_horizontal = expanding to resize it (or layoutpolicy_vertical = expanding in a vbox)
  • Accepts datamodels (to create lists from game data).

dynamicgridbox

  • Is only used with datamodels.
  • Arranges all the items vertically. Use flipdirection = yes to make it horizontal.
  • Doesn't ignore invisible items by default. Use ignoreinvisible = yes to change it.
  • Can be fixed size, resized by the content and limited by minimumsize and maximumsize.
  • Items can be of different size.
  • Can become laggy with very long lists.

fixedgridbox

  • Similar to a dynamic box but all its itmes are of fixed size (it's essentially a table).
  • Is only used with datamodels.
  • Arranges all its items vertically. Use flipdirection = yes to make it horizontal.
  • Cannot ignore invisible items.
  • Can be fixed size, resized by the content and limited by minimumsize and maximumsize.
  • Much better for performance with long lists.

overlappingitembox

  • Is only used with datamodels.
  • Arranges all its items horizontally and overlaps them if the list is longer that the size of the box. Use flipdirection = yes to make it horizontal.
  • Can be fixed size or autoresized.

scrollarea

  • A widget with scrollbars that appear if the content is bigger than its size.
  • Scrollbars can be disabled with scrollbarpolicy_horizontal = always_off and scrollbarpolicy_vertical = always_off.
    • A scrollarea with no scrollbars can be used to crop lists or images.
  • Can be fixed size, resized by the content and limited by minimumsize and maximumsize.

button

  • A clickable object. Accepts onclick and onrightclick.
    • When adding a right click function, include button_ignore = none.
  • Doesn't have a texture by default.
  • Can be fixed size or resized by its children.
    • A 0x0 button can be used to add invisible hotkeys.

icon

  • Displays a texture.
  • Can be used as a widget to store children.
  • Can be flipped with mirror = horizontal or mirror = vertical.

textbox

  • Shows text.
  • Can be fixed size or autoresized.
  • Use elide = right or elide = left to cut off text that is too long
  • Can be a single line or multiple, with multiline = yes.
  • Game files often use templates set in gui/shared/text.gui, like text_single. Use them to keep visual consistency and to type less code every time.

Promotes and Functions

Each window has a predefined set of commands - promotes and functions - available for it. These can be found in the data_types.log file in Documents/Paradox Interactive/Crusader Kings III/logs/ after you use the DumpDataTypes console command.

They are used to display all data from the game, like your name, gold, children, and to set button actions.

A promote returns a scope, i.e a game object, like Character or Province, while a function returns a number, a string or a boolean (true/false) value, etc.

Global commands can be used anywhere, like GetPlayer (returns the player character) or GetCurrentDate.

Other commands can only be used in their window/object, for example, GetParents can only be used in the character window and must be started with CharacterWindow.GetParents.

Commands can be chained like this:

CharacterWindow.GetCharacter.GetPrimaryTitle.GetHeir.GetPrimarySpouse.GetFather

Children can inherit the scope from their parent, meaning we wouldn't need to retype the line above to show information about this character. Instead we can set datacontext of a widget to this line and then every textbox in it will use "[Character.GetNameNoTooltip]", "[Character.GetGold]", etc.

The same applies to items in gridboxes.

Templates

Templates are named blocks of code which can be used multiple times throughout the code, which helps maintain the same style and reduce the amount of code we write. Editing the template will edit all instances of it!

Templates are global and can be defined in any file. Most of the game templates are stored in gui/shared. A local version, local_template, has to be defined within the same file.

Templates can store the contents of an entire window or just one line, like this one:

template Window_Size_Sidebar
{
		size = { 610 100% }
}

This template can be used inside another element with "using = Window_Size_Sidebar", which will, essentially, replace the "using" line with the contents of the template.

Types

If templates can contain just a few properties, types are always whole elements, like a button or a widget.

text_single and text_multi are types of a textbox with many properties already defined for them, so we don't need to retype them every time and instead simply write:

text_single = {
	text = "my text"
}

Types are defined in a slightly different way, by creating a named group of types first:

types Standard_Types
{
	type text_single = textbox {
	...
	}
}

Blockoverride

Templates and types can have named override blocks, which allow us to edit a part of an instance without changing the whole template. For example, a template may have a default block of text:

block "text"
{
	text = "default_text"
}

To replace it, we add a blockoverride with the same name in our instance:

blockoverride "text"
{
	text = "actual text"
}

We can also remove it from our instance like this: blockoverride "text" {}

Scripted GUIs

Scripted guis are, essentially, hidden events triggered from the UI.

The are stored as .txt files in game/common/scripted_guis and cannot be reloaded from the game, unlike the .gui files.

The basic structure of a scripted gui is:

gui_name = { 
	scope = character 		# the root scope, i.e. the target of the effects
	saved_scopes = {} 		# any additional targets

	is_shown = {} 		# is it visible on the UI?

	ai_is_valid = {} 		# is the AI allowed to use it? Disabled by default.

	is_valid = {} 		# can the player use it?

	effect = {			# what it does
		custom_tooltip = ""	# adds a tooltip
	}	 			
}

Not all of the blocks are necessary. Sometimes a scripted gui may only contain the scope and is_shown or effect.

In the .gui file we use the following:

datacontext = "[GetScriptedGui('gui_name')]"

onclick = "[ScriptedGui.Execute( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

visible = "[ScriptedGui.IsShown( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

enabled = "[ScriptedGui.IsValid( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

tooltip = "[ScriptedGui.BuildTooltip( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

datacontext is necessary to link the element to the scripted gui. Other commands won't work without it.

In this example the scripted gui is scoped to the player with a global function GetPlayer.MakeScope. It can be changed to someone in the character window, e.g. CharacterWindow.GetCharacter.MakeScope, or if the scope was a province, HoldingView.GetProvince.MakeScope.

ScriptedGui.Execute is used with buttons and will execute everything listed in the effect block in the scripted gui.

IsShown and IsValid check for conditions in is_shown and is_valid blocks.

BuildTooltip can also be used with a textbox to display custom text in the UI.

To save another scope to our scripted gui, we use AddScope like this:

"[ScriptedGui.Execute( GuiScope.SetRoot( GetPlayer.MakeScope ).AddScope('target', CharacterWindow.GetCharacter.MakeScope ).End )]"

And then we use the same name in the scripted gui:

saved_scope = {
  target
}

Multiple scopes can be saved this way.

It is important to not put any spaces before the dots or opening parentheses! Execute( is correct, Execute ( is not. Other spaces can be omitted, but they help with readability.

Displaying a variable or script value

Variables and script values can be shown on the UI like this:

variable: text = "[GetPlayer.MakeScope.Var('test_var').GetValue|1]"

svalue: text = "[GuiScope.SetRoot( GetPlayer.MakeScope ).ScriptValue('test_value')|0]"

In this example the variable is stored in the player character and is called test_var. Any other scope can be used, just remember to add MakeScope.

|1 at the end is optional and will cut off all decimals save for one, so instead of 1.573 you will see 1.5. Note that it does not round the value. You can set this at any number, add % to convert to a percentage, add = or + to color the value if it's positive or negative.

When using localization to display values in events, we use this:

event_var: "[ROOT.GetCharacter.MakeScope.Var('test_var').GetValue|0]"

event_value: "[SCOPE.ScriptValue('test_value')|0]"

If you have a saved scope (named "target" here), it changes to this:

event_var: "[target.MakeScope.Var('test_var').GetValue|0]"

event_value: "[GuiScope.SetRoot( target.MakeScope ).ScriptValue('test_value')|0]"

Displaying data lists

We can create custom lists of characters, for example, to make societies.

To do this, first, we need to add them to a variable list. This can be done though an event or a scripted gui, like this:

effect = {
	every_living_character = {
		limit = {
			has_trait = paranoid
		}

		root = {
			add_to_variable_list = {
				name = secret_society
				target = prev
			}
		}
	}
}

If we fire this effect for the player, they will be the root, so the list will be stored in them.

Then we use any list box (vbox, dynamicgridbox, fixedgridbox) with the datamodel set to our list:

dynamicgridbox = {
	datamodel = "[GetPlayer.MakeScope.GetList('secret_society')]"

	item = {
		flowcontainer = {
			datacontext = "[Scope.GetCharacter]"

			portrait_head_small = {}

			text_single = {
				text = "[Character.GetNameNoTooltip]"
			}
		}
	}
}

New windows and toggles

There isn't a simple way to create a new window and make your mod compatible with others. We have to either overwrite the hud or other windows.

The usual way is to add the new window to hud.gui inside of ingame_topbar widget and then add a button to show/hide it.

The alternative is to rewrite one of the unused windows, like test_gui and add a button with the console command that shows it.

Toggles with PdxGuiWidget

PdxGuiWidget is a simple function used to hide or reveal named elements.

In this example we have a container with a hidden submenu, one button that shows it and another that hides it.

  1. when clicked, the first button goes back to its parent, searches in it for the submenu and the other button, reveals them and hides itself
  2. the second button searches for the element and the button, reveals them and hides itself
container = {
	button = {
		name = "show submenu"

		onclick = "[PdxGuiWidget.AccessParent.FindChild('submnenu').Show]"
		onclick = "[PdxGuiWidget.AccessParent.FindChild('hide submenu').Show]"
		onclick = "[PdxGuiWidget.Hide]"
	}
		
	button = {
		name = "hide submenu"
		visible = no

		onclick = "[PdxGuiWidget.AccessParent.FindChild('submnenu').Hide]"
		onclick = "[PdxGuiWidget.AccessParent.FindChild('show submenu').Show]"
		onclick = "[PdxGuiWidget.Hide]"
	}

	widget = {
		name = "submenu"
		visible = no
	}
}

If the elements are separated by more parents/children, we can repeat AccessParent like this:

onclick = "[PdxGuiWidget.AccessParent.AccessParent.AccessParent.AccessParent.FindChild('submnenu').Show]"

Each button can hide or reveal multiple elements of any type. You only need to provide the name.

Pros:

  • easy to edit on the fly as any changes can be reloaded with the "reload gui" command

Cons:

  • the toggles will reset any time the game is restarted
  • If you want to hide multiple things or toggles, the code will get very bloated and hard to manage
  • trying to hide entries in a data list (like a dynamicgridbox) will only hide the first instance

Toggles with animation

We can set up animations that are triggered by buttons (or conditions) to hide/show elements or to even move them. This way we don't need to count how many parents separate the button and the window and we can trigger many things at once with just one onclick.

The previous example would look like this and work the same way. The first button triggers "show_submenu", which hides the button and shows the rest, while the second button triggers "hide_submenu", which hides this button and the widget and shows the first button.

container = {
	button = {

		state = { # this is an animation
			name = show_submenu
			on_start = "[PdxGuiWidget.Hide]"
		}
		state = {
			name = hide_submenu
			on_start = "[PdxGuiWidget.Show]"
		}

		onclick = "[PdxGuiTriggerAllAnimations('show_submenu')]"
	}
		
	button = {
		visible = no

		state = {
			name = show_submenu
			on_start = "[PdxGuiWidget.Show]"
		}
		state = {
			name = hide_submenu
			on_start = "[PdxGuiWidget.Hide]"
		}

		onclick = "[PdxGuiTriggerAllAnimations('hide_submenu')]"
	}

	widget = {
		visible = no

		state = {
			name = show_submenu
			on_start = "[PdxGuiWidget.Show]"
		}
		state = {
			name = hide_submenu
			on_start = "[PdxGuiWidget.Hide]"
		}
	}
}

it is longer, but animations can be saved as templates and reused with one line, like using = hide_animation. Fullscreen Barbershop uses animations extensively, if you want a better example.

Pros:

  • can also be edited on the fly, with "reload gui" command
  • easier to link many things together, and even open a different window and trigger an animation in it

Cons:

  • animation blocks can be quite lengthy
  • all toggles will reset when the game is restarted

Toggles with Scripted GUIs

Scripted guis allows us to use script to toggle visibility. They make it easier to manage multiple things but can impact performance when used in big numbers.

For a scripted toggle we need to create a .txt file in common/scripted_guis/. The name of the file can be anything.

A basic toggle in this file would looks like this:

gui_toggle = {
	scope = character
	
	is_shown = {
		has_variable = gui_toggle
	}
	
	effect = {
		if = {
			limit = {
				has_variable = gui_toggle
			}
			remove_variable = gui_toggle
		}
		else = {
			set_variable = gui_toggle
		}
	}
}

When clicked, it adds a variable to the scoped character and removes it when clicked again.

Then, if the variable is present, our window will be visible. If it's not, it's hidden.

This is how the gui file would look like. We can use just one button as its function will change with each click.

container = {
	button = {
		datacontext = "[GetScriptedGui('gui_toggle')]"
		onclick = "[ScriptedGui.Execute( GuiScope.SetRoot( GetPlayer.MakeScope ).End )]"
	}

widget = {
	datacontext = "[GetScriptedGui('gui_toggle')]"
	visible = "[ScriptedGui.IsShown( GuiScope.SetRoot( GetPlayer.MakeScope ).End )]"
	}
}

"GetPlayer" is a global promote and returns the player character.

"Player.MakeScope" makes our player the scope of the scripted gui, meaning this is where we store the variable in.

Two buttons can be used if you want different tooltips for them. In this case, copy the 'visible' property to both buttons, but add 'Not' to one of them, so it's hidden by default:

visible = "[Not(ScriptedGui.IsShown( GuiScope.SetRoot( GetPlayer.MakeScope ).End ))]"

Pros:

  • the toggles are saved in the character and won't reset unless they die or you start a new game
  • easier to link many things, even in different windows

The downsides:

  • you need to restart the game to update scripted guis
  • it's a little harder to remember the syntax (copy the code from here to reduce the chance of mistakes)