Skip to content

Widget creation

johan-buret edited this page Nov 1, 2023 · 9 revisions

What is a widget ?

A widget is a graphical element on the screen. It is usually a 2D image (say a button), but in civ4 engine terminology it can include text elements too.

A lot of the widgets are purely decorative, but some have actions attached to them such as (but not limited to) mouse clicking, drag-and-drop, mouse over text.

Example of a widget

Here is a text widget, which has the ability to display text whenever the mouse hover over the widget. The widget in question is WIDGET_HELP_TAX_CALCULATION, which is used consistently as an example on this page.

image

What data does a widget contain

When widgets are added, usually it included 3 variables:

WidgetTypes eWidgetType, int iData1, int iData2 

Whenever an action happens to a widget (any widget), code is called, which identifies the widget based on those data and then executes the action in question. WidgetTypes is used as the overall category while the ints are used to provide details about the specific widget. For instance the WidgetTypes can tell that it has something to do with yields while iData1 then details which YieldTypes the specific widget is linked to. The meaning of iData1 and iData2 depends on which WidgetTypes is used.

The issue with WidgetTypes.GENERAL

Long story short : WidgetTypes.GENERAL has neither meaning nor behaviour attached.

The main advantage of WidgetTypes.GENERAL is that it doesn't bind the graphical component to a predetermined behaviour when clicked, hovered, or dragged and dropped.

This feature becomes an issue when we want a component to have the same behaviour across all screens, or having access to state not exposed via the Python API. It forces to make calls that are slow, forces to duplicate game logic on both Python script and C++. All of this is explained in issue #929

How to create a widget

In our example, we will create the widget for Tax Information, the one displayed at the top of Africa and Europe Its name will be WIDGET_HELP_TAX_CALCULATION

Declare the widget in GlobalTypes.xml

The path of the file within the mod is Assets/XML/GlobalTypes.xml

image

This list will be added to the end of the vanilla WidgetTypes. Since only the vanilla widgets have restrictions regarding ordering, widgets mentioned in xml can be of any order.

Important

Recompiling the DLL is necessary to take the new widget type into account

C++

Most of the declarative work takes place in CvDLLWidgetData.cpp

template<> // This lines tells the compiler we use a fully defined template
class WidgetContainer<WIDGET_HELP_TAX_CALCULATION> : public WidgetData
{

Everything regarding the widget will be kept in a single class as that makes it much easier to look up the specifics about a specific WidgetTypes. The code here is the standard for all widgets, through obviously the widget type will differ.

public:
	WidgetContainer(const CvWidgetDataStruct widgetDataStruct)
		: eColonyPlayer((PlayerTypes)widgetDataStruct.m_iData1)
		, bForceDetailedInfo(widgetDataStruct.m_iData2 == 1) //If set to 1, will display the detailed info, regardless of hidden variable display option
	{}

	const PlayerTypes eColonyPlayer;
	const bool bForceDetailedInfo;

This saves iData1 and iData2. This allows naming and types for those two variables, which will be consistent for all actions. It's not required to use both, or in some cases none of them will be needed and the constructor can be skipped entirely.

At this point the widget is fully declared in C++ and will be called whenever it should be called. The only step left is to add actions.

Adding actions in C++

// Attention, this function is defined outside the scope of the class WidgetContainer<WIDGET_HELP_TAX_CALCULATION>
WidgetData* WidgetData::getNew(const CvWidgetDataStruct& widgetData)
{
	switch (widgetData.m_eWidgetType)
	{
	case WIDGET_PEDIA_JUMP_TO_UNIT: return new WidgetContainer<WIDGET_PEDIA_JUMP_TO_UNIT>(widgetData);
	case WIDGET_HELP_TAX_CALCULATION: return new WidgetContainer<WIDGET_HELP_TAX_CALCULATION >(widgetData); // our example widget
	}

	return NULL;
}

Each widget has to return an instance in this function, so adding a widget means adding a single line in this function. Each possible action has a dedicated function. It overrides the base class function, which does nothing meaning anything not mentioned will just do nothing.

Function Description
void generateMouseOverText(CvWStringBuffer &szBuffer) This allows to fill a text zone that appears when hovering a point of interest, such as an unit, or the tax rate display to get the details of the calculation
bool onLeftCLick() The action that should be taken on a left click
bool onRightClick() The action that should be taken on a right click
bool onDoubleLeftClick() The action that should be taken on a double click
bool onDragAndDropOn(const CvWidgetDataStruct destinationWidgetData) Widgets added with addDragableButton or addDragableButtonAt can be dragged around. Ending the dragging will call this where destinationWidgetData is the widget the original one landed on top of
bool isPediaLink() Tells if clicking the widget provides a valid link to a Colopedia page
	void parseHelp(CvWStringBuffer& szBuffer) const
	{
		CvPlayerAI& kColony = GET_PLAYER(eColonyPlayer);
		CvPlayerAI& kKing = *kColony.getParentPlayer();
		const bool bNoHidden = GC.getGameINLINE().isOption(GAMEOPTION_NO_MORE_VARIABLES_HIDDEN);
		if (bNoHidden || bForceDetailedInfo)
		{
			const int chancePerThousand = kKing.getTaxRaiseChance();
			szBuffer.append(gDLL->getText("TXT_KEY_TAX_BAR",
				kKing.getFullYieldScore(), //apply the Global Ratio
				kKing.getTaxThresold(), // get a comparable quantity
				GC.getYieldInfo(YIELD_TRADE_GOODS).getChar(),
				chancePerThousand / 10,	chancePerThousand % 10,
				GLOBAL_DEFINE_TAX_RATE_RETAINED_FRACTION
				));
		}
		else
		{
			const CvWString st = gDLL->getText("TXT_KEY_MISC_TAX_RATE", 
				kColony.getTaxRate(), 
				kColony.NBMOD_GetMaxTaxRate());

			szBuffer.append(st);
		}
	}
};

Python invocation

When placing graphical elements with Python calls, there are 3 parameters at the end.

screen.setText(self.getNextWidgetName(), "Background", szTaxRate, CvUtil.FONT_RIGHT_JUSTIFY,
    self.XResolution - CyInterface().determineWidth(szExit) - self.STANDARD_MARGIN * 2, self.STANDARD_MARGIN, 
    0, FontTypes.TITLE_FONT,
    WidgetTypes.WIDGET_HELP_TAX_CALCULATION, self.player_id, -1 )  # here

The three parameters on the last line correspond to the CvWidgetDataStruct, in that order the WidgetTypes eWidgetType, int iData1 and int iData2

Doing so will attach the behaviour of the C++ class we defined above to this zone of text.