How To: Display a Blank Civ4 Screen

Jeckel

Great Reverend
Joined
Nov 16, 2005
Messages
1,637
Location
Peoria, IL
Working with GUIs is not easy, but once you get the hang of it you'll find its not to bad. The first time I tryed working with Civ4 screens I banged my head against the wall. The problem was I knew absolutly nothing about GUI programming except the simple fact that clicking on buttons makes the computer do things. :lol:
If you have never worked with GUI programing of any type then you should open another browser window and google it, check wikipedia, read a few things about the concept of GUI programing before you try to jump into it.

That said, here is how to make and display a blank screen. :)



STEP 0: Before we Start, A Little On GUI Terms
Widget - A widget is a general term to discribe any object a GUI displays on the screen.

Surface - Surfaces are a mainstay of 2d graphics and is basicly a background that we put widgets on.

Window - A window is really just a suface with a nice graphical border around it.

Screen - A screen is a window or surface displayed over the Civ4 game.

Dirty - A widget is dirty if its appearance has change and Civ4 doesn't know it.

DirtyBits - Civ4 term for dirty widgets and groups of dirty widgets.



STEP 1: Lets Make a Screen File

Directory: \TutorialBlankScreen\Assets\Python\Screens\
File: TutorialBlankScreen.py

1) Staring at the Desktop Won't Help

First, obviously, make the file and open it.

Now that you are looking at the empty file, lets put some stuff in it.

2) Some Header Text

Its always nice to have basic info at the top.

Code:
#
# TutorialBlankScreen Mod
# by Your Name
# 
#

3) Import Modules

We are going to import everything from CvPythonExtensions into our module's local namespace and we also import CvScreenEnums.

Code:
from CvPythonExtensions import *
import CvScreenEnums

4) Global Attributes

Here I instance CyArtFileMgr and CyGlobalContext and set them to an attributes. We only use ArtFileMgr once in this tutorial, but in most screens its worth doing here instead of instanting the class every time in the code.

Code:
############################################################################
# globals
gc = CyGlobalContext()
ArtFileMgr = CyArtFileMgr()
############################################################################

5) Declare Screen Class

Next we have to declare the class and add a nice comment line telling what the class is.

Code:
class CvTutorialBlankScreen:
	"A Blank Civ4 Screen"

6) Declare __init__ Method

When the screen object is created the __init__ method will be called first by Civ4 and is where we will put variables our screen will use in its other code.

Code:
	def __init__(self):

7) Unique String Identifier Prefix

The one fundamentals of GUI programing is the use of strings to identiy displayed objects. Civ4 is no different. So now we need to come up with some string that we can use to identify widgets for this screen. All widgets will have this string as the first part of their unique id string.

Code:
		self.sWidgetPrefix = "TutorialBlankScreen"

8) Unique String Identifiers

We are going to be using
Now that we have our prefix, we set unique id strings for the two widgets we are going to be using.

Code:
		self.MANAGER_SCREEN_ID = self.sWidgetPrefix + "MainWindow"
		self.BACKGROUND_ID = self.sWidgetPrefix + "BackgroundImage"

9) Xs and Ys

When we put widgets on the screen we have to tell it where to put them. This is controlled with X and Y coordinates. Unlike many GUI systems, 0 X and 0 Y are at the Top Left of the monitor.

Code:
		self.X_SCREEN = 0
		self.Y_SCREEN = 0

10) Declare getScreen Method

This method is what you will use in other code to get access to CyGInterfaceScreen methods which are the work horse of Civ4 GUI programing. It takes no arguments and returns our screen's CyGInterfaceScreen object.

NOTE: This method uses the MANAGER_SCREEN_ID attribute we set above and the CvScreenEnums module we imported at the beginning.

Code:
	def getScreen(self):
		return CyGInterfaceScreen(self.MANAGER_SCREEN_ID, CvScreenEnums.TUTORIAL_SCREEN_BLANK)

11) Declare hideScreen Method

Ok, we are not going to use this method in this tutorial, but I point it out because you will find it or a version of it in almost every Civ4 screen.

This method will close your screen. Generally most screens will have an exit button (which I will cover in the next tutorial). However, an exit button is an "exit button widget" and there are times you want some other type of widget to also close the screen. For example, in the Military Advisor Screen, when you click on the minimap it closes the screen and goes to that tile. Since the minimap is a "mimimap widget" you have to close the screen by calling this function.

NOTE: The first thing we do is use our getScreen method to get our screen's GInterface object. Then we call its hideScreen method to close the screen.

Code:
	def hideScreen(self):
		screen = self.getScreen()
		screen.hideScreen()
		return None

12) Declare update Method

IMPORTANT!!: You MUST declare this method receiving a single argument or your screen will throw an exception every frame. This means dozens of error popup windows for each second that passes before you alt-tab and fix the problem. Might as well end-task it and restart the game.

When your screen is being displayed, this method is called every frame. I don't know what to say about the use of this method. It definetly has its place, but, as is with any method that the SDK calls on milliseconds, I would be careful of how much code you shove in here.

One last thing. In all the standard Civ4 screen files, they end this method with a lone "return", you should always "return None" if a method returns nothing. While Python will automaigicly return a None object for you, it is good coding practice to place it explicitly.

Code:
	def update(self, fDelta):
		return None

13) Declare handleInput Method

IMPORTANT!!: You MUST declare this method receiving a single argument or your screen will throw an exception each time you try to open it and, surprisingly enough, willnot handle input.

This is the workhorse of interactive screens. When players send input to the game while your screen is up, this method is where you will catch the clicks and key strokes. The single argument, here "inputClass", will contain all the info you will need to know.

Code:
	# handle the input for this screen...
	def handleInput(self, inputClass):

14) Attributes For Later Use

IMPORTANT: You must have some line of code, not just comments under a method declaration or you will get an exception.

We are going to set the iNotifyCode attribute. Mainly I want to show the getNotifyCode() method. The inputClass argument is a ScreenInput object and has a few important pieces of generally info and possibly data from the widgets that were clicked or keys that were hit.

List of ScreenInput Instance Methods.
Spoiler :
getNotifyCode()
getFunctionName()

getButtonType()
getData()
getData1()
getData2()
getFlags()
getID()
getMouseX()
getMouseY()
getOption()
getPythonFile()
isAltKeyDown()
isCtrlKeyDown()
isShiftKeyDown()


Code:
		iNotifyCode = inputClass.getNotifyCode()

Now we know what NotifyCode we are dealing we, but we need things to compare it against. For these we are going to use the "NotifyCode". For those that arn't to familiar with Python and have a quick eye, you will not we have not declared this variable anywhere in our file, nor so we prefix it with a module attribute and a dot. If you remember the first line of code in this file (from CvPythonExtensions import *), it basicly loaded all of its variables into our file as if we had declared them ourselves, so we can saftly assume it comes from that module.

List of NotifyCode attributes
Spoiler :
NotifyCode.NOTIFY_CHARACTER
NotifyCode.NOTIFY_CLICKED
NotifyCode.NOTIFY_CURSOR_MOVE_OFF
NotifyCode.NOTIFY_CURSOR_MOVE_ON
NotifyCode.NOTIFY_DBL_CLICKED
NotifyCode.NOTIFY_FLYOUT_ITEM_SELECTED
NotifyCode.NOTIFY_FOCUS
NotifyCode.NOTIFY_LINKEXECUTE
NotifyCode.NOTIFY_LISTBOX_ITEM_SELECTED
NotifyCode.NOTIFY_MOUSEMOVE
NotifyCode.NOTIFY_MOUSEWHEELDOWN
NotifyCode.NOTIFY_MOUSEWHEELUP
NotifyCode.NOTIFY_MOVIE_DONE
NotifyCode.NOTIFY_NEW_HORIZONTAL_STOP
NotifyCode.NOTIFY_NEW_VERTICAL_STOP
NotifyCode.NOTIFY_SCROLL_DOWN
NotifyCode.NOTIFY_SCROLL_UP
NotifyCode.NOTIFY_SLIDER_NEWSTOP
NotifyCode.NOTIFY_TABLE_HEADER_SELECTED
NotifyCode.NOTIFY_UNFOCUS


Code:
		iNotifyClicked = NotifyCode.NOTIFY_CLICKED
		iNotifyChar = NotifyCode.NOTIFY_CHARACTER

15) Just For Fun

Just for fun we are going to parse some input and find a specific widget.

First, find clicked events using the variables we just set.

Code:
		if(iNotifyCode == iNotifyClicked):

16) Need Somthing Under the If Statement

Now we need to find our background widget. I use another inputClass method here, getFunctionName, which will return the unique string id of the widget that was clicked. However, if the NotifyCode is not valid, getFunctionName, and many of the other ScreenInput object methods may return None.

NOTE: This code will never acually be run in this tutorial as the two widgets, our screen and our background, don't register being clicked.

Code:
                        sInputName = inputClass.getFunctionName()

17) Finish handleInput Method

All the default screens I've check simply return 0 at the end of the method, but, and I'm just guessing, I think if you return 1 it would consume the event. If you have no idea what that means, then just return 0 and you should be fine.

Code:
		return 0

18) Declare interfaceScreen Method

We declare the method and get our CyGInterfaceScreen object. I also include an if statement to check if the screen is active. This is avoid the screen being displayed multiple times.

Code:
	def interfaceScreen(self):
		screen = self.getScreen()
		if (screen.isActive()):
			return None

19) Set Screen Size and Position

This is the most important method you will call, as without it your screen will have no size to display or a place to display it at.

It takes 4 integer arguments.

The first two are the screen's width and height, in that order.

The third argument is the number of pixels from the left side of the monitor.

The forth argument is the number of pixels from the top of the monitor.

NOTE: If you wish to center the screen the replace "self.X_SCREEN" with "screen.centerX(0)" and replace "self.Y_SCREEN" with "screen.centerY(0)". This will line up the center of your screen with the center of the main game window.

Code:
		screen.setDimensions(self.X_SCREEN, self.Y_SCREEN, self.W_SCREEN, self.H_SCREEN)

20) Adding A Background

If you saved now and tryed to run the code as it is up to this point, the game would run, but the screen wouldn't show for multiple reasons (Mainly because we havn't bound it to a key press or called the method to acually display all our widgets). However, assuming we had all the other code in place, the screen that would be displayed would be completly invisible. So, lets solve that by adding a background.

First we need to get the path string to the background dds image file. We do this using the ArtFileMgr variable we set at the top of our file. Its getInterfaceArtInfo method takes one string as an agrument. The string must be a valid InterfaceArtInfo Type tag as defined in the CIV4ArtDefines_Interface.xml file. Don't worry about having it defined right now. That will be taken care of in STEP 2.

Code:
		sBackgroundPath = ArtFileMgr.getInterfaceArtInfo("TUTORIAL_BLANK_SCREEN_BACKGROUND").getPath()

Now we call the method that will add our dds image to our screen. It takes several arguments.

First it requires a unique string id, which we already have set.

Second it needs a valid path string, which we just set.

Third and Forth are the X and Y pixel to place the top left corner of the image.

Fifth and sixth are width and hight to make the image.

Seventh and eighth is for the user to pass their own integer values. These values can then be retrieved later with various getData1 and getData2 methods. For example in our screen's handleInput method, the ScreenInput object can return this data.

Code:
		screen.addDDSGFC(self.BACKGROUND_ID, sBackgroundPath, 0, 0, self.W_SCREEN, self.H_SCREEN, WidgetTypes.WIDGET_GENERAL, -1, -1)

Lastly I would like to note that you don't have to use a background image. The Domestic Advisor Screen doen't set any kind of background image because it is all widgets. Likewise the City Screen has no background image since you want to be able to see the city in the middle.

21) Are We Done Yet?

No we arn't done yet, but we are close.
We need to do three more things to finish this off.

First add this line. I don't know what it does. From the little testing I did, passing True or False has no different effect on the screen as it is.

NOTE: I do this first because in all the standard Civ4 screens I've checked it is always done before the next two lines of code.

NOTE: I checked and the ; at the end of this line is not a typo. I don't know what it signifies, but I don't know any reason to remove it either.

Code:
		screen.setRenderInterfaceOnly(True);

This is the important part. This line is what tells Civ4 to display your screen on the game window.

The first argument must be a valid PopupStates enum. Just stick with PopupStates.POPUPSTATE_IMMEDIATE unless you know what else to put in there.

The second argument can be True or False. If it is True, any events that your screen doesn't consume will be passed to the game engine. For example, if it is set to True, you raise your screen, roll the mouse wheel down, and then close your screen, you will see that your rolling of the mouse wheel has inadvertly zoomed you out away from the game world. Setting the argument to False will keep input from reaching anything other then your screen while it is up.

Code:
		screen.showScreen(PopupStates.POPUPSTATE_IMMEDIATE, False)

Lastly is this line of code. Again, I have no idea what this line of code does or what is different if you pass a False instead of a True. I put it here because all the standard Civ4 screens I check have this line with a True argument if they set a background image.

Code:
		screen.showWindowBackground(True)



STEP 2: Telling Civ4 You Added A Screen

If you don't know how to do this, I have already written a tutorial devoted to accomplishing this.

How To: Add New Screens to Civ4

After you have completed that move on to STEP 3.



STEP 3: Displaying Your Screen InGame

Directory: \TutorialBlankScreen\Assets\Python\
File: CvEventManager.py

1) Find the onKbdEvent Method

Open the CvEventManager.py file and find the part of the code that looks like this.

Code:
#################### ON EVENTS ######################
	def onKbdEvent(self, argsList):
		'keypress handler - return 1 if the event was consumed'

		eventType,key,mx,my,px,py = argsList
		game = gc.getGame()
		
		if (self.bAllowCheats):
			# notify debug tools of input to allow it to override the control
			argsList = (eventType,key,self.bCtrl,self.bShift,self.bAlt,mx,my,px,py,gc.getGame().isNetworkMultiPlayer())
			if ( CvDebugTools.g_CvDebugTools.notifyInput(argsList) ):
				return 0
		
		if ( eventType == self.EventKeyDown ):
			theKey=int(key)
			
			CvCameraControls.g_CameraControls.handleInput( theKey )
						
			if (self.bAllowCheats):
				# Shift - T (Debug - No MP)
				if (theKey == int(InputTypes.KB_T)):
					if ( self.bShift ):
						self.beginEvent(CvUtil.EventAwardTechsAndGold)
						#self.beginEvent(CvUtil.EventCameraControlPopup)
						return 1
							
				elif (theKey == int(InputTypes.KB_W)):
					if ( self.bShift and self.bCtrl):
						self.beginEvent(CvUtil.EventShowWonder)
						return 1
							
				# Shift - ] (Debug - currently mouse-overd unit, health += 10
				elif (theKey == int(InputTypes.KB_LBRACKET) and self.bShift ):
					unit = CyMap().plot(px, py).getUnit(0)
					if ( not unit.isNone() ):
						d = min( unit.maxHitPoints()-1, unit.getDamage() + 10 )
						unit.setDamage( d, PlayerTypes.NO_PLAYER )
					
				# Shift - [ (Debug - currently mouse-overd unit, health -= 10
				elif (theKey == int(InputTypes.KB_RBRACKET) and self.bShift ):
					unit = CyMap().plot(px, py).getUnit(0)
					if ( not unit.isNone() ):
						d = max( 0, unit.getDamage() - 10 )
						unit.setDamage( d, PlayerTypes.NO_PLAYER )
					
				elif (theKey == int(InputTypes.KB_F1)):
					if ( self.bShift ):
						CvScreensInterface.replayScreen.showScreen(False)
						return 1
					# don't return 1 unless you want the input consumed
				
				elif (theKey == int(InputTypes.KB_F2)):
					if ( self.bShift ):
						import CvDebugInfoScreen
						CvScreensInterface.showDebugInfoScreen()
						return 1
				
				elif (theKey == int(InputTypes.KB_F3)):
					if ( self.bShift ):
						CvScreensInterface.showDanQuayleScreen(())
						return 1
						
				elif (theKey == int(InputTypes.KB_F4)):
					if ( self.bShift ):
						CvScreensInterface.showUnVictoryScreen(())
						return 1
                                # < TutorialBlankScreen Mod Start 1/1 >
				elif (theKey == int(InputTypes.KB_F10)):
					#CvScreensInterface.showTutorialBlankScreen()
                                        import CvTutorialBlankScreen
                                        CvTutorialBlankScreen.CvTutorialBlankScreen().interfaceScreen()
					return 1
                                # < TutorialBlankScreen Mod End 1/1 >
											
		return 0

2) Find Where to Add Our Code

Now find the specific lines.

Code:
		if ( eventType == self.EventKeyDown ):
			theKey=int(key)
			
			CvCameraControls.g_CameraControls.handleInput( theKey )

3) Check For the F10 Key

We do 3 things in this little block of code.

First we check if the key that was pressed is the F10 key. I use the F10 because standard Civ4 doesn't use it.

The second line of code uses the CvScreensInterface module that is imported in this file and calls the showTutorialBlankScreen method we defined in it durning STEP 2.

The third code line is the return. By returning 1 you will consume this event so no other code will act upon it. What this means is, if you set your screen to show on F1 and return 1, then the Domestic Advisor Screen will not show. However, if you return 0, then at best only the Domestic Advisor Screen would show and at worst both screens would be displayed. But if you don't need to show multiple screens with one key stroke then you can do it.

Code:
                        # < TutorialBlankScreen Mod Start 1/1 >
			if (theKey == int(InputTypes.KB_F10)):
                                CvScreensInterface.showTutorialBlankScreen()
				return 1
                        # < TutorialBlankScreen Mod End 1/1 >

3.1) Another Way To Display Your Screen

There is another way to display your screen that is much more friendly to debuging your screen. If you are interested read the following spoiler.

Spoiler :
When you start a game, CvScreensInterface creates an instance of your screen. When you then call showTutorialBlankScreen to display your screen it uses that same instance. This is good when your release your mod, but when writing code and testing the screen it has a down side that when you alt-tab and change some code your changes won't be used because it is displaying an instance of your screen with the same code the file had when you started the game.

To get around this we need to create a new instance of our screen each time the game displays it. To accomplish this we need to do two things.

First, at the top of CvEventManager.py, under all the import statements, add the following line. We are importing our screen's module.

Code:
# < TutorialBlankScreen Mod Start 1/2 >
import CvTutorialBlankScreen
# < TutorialBlankScreen Mod End 1/2 >

Now, instead of the code in part 3, add this to the onKbdEvent method in the same place.

Code:
                        # < TutorialBlankScreen Mod Start 1/2 >
			if (theKey == int(InputTypes.KB_F10)):
                                CvTutorialBlankScreen.CvTutorialBlankScreen().interfaceScreen()
				return 1
                        # < TutorialBlankScreen Mod End 1/2 >
 
Here is the mod I wrote and based this tutorial on.
JCiv4ScreenTutorial Mod Download Page


The next tutorial will show how to put and use wigdets to make a information display screen.
The one after that will teach how to handle input from those widgets.

Don't hold your breath or anything, it may be a month or so before I get the next one out. :)
 
Thanx jdog5000, always glad to give back. :)

I am planning the next tut, but the MMORPG project I'm working on just finallized an underlying game engine and on top of testing that I am working on the 3D concept art. If I get a couple free hours and need a break from the project I will see about the next installment of these tutorials. :king:
 
Does anyone know where the python code that controls the religion screen is?
I just cant seem to find it, all the other screens are pretty self evident but I cant find the religion one.
All I want to do is make it so that it can properly display more than 7 religions, ideally about 10 or so.

Edit - No worries, found this
http://forums.civfanatics.com/downloads.php?do=file&id=8539
seems to work fine
thanks to Johny Smith
 
Jeckel, I have a question for you. :)

Is there a way to call a function every time I close a specific screen (Europe screen for C4:Col that is)? I see there is a widget type called WIDGET_CLOSE_SCREEN but that is working only for exit button, not escape. :sad:

Thank you.
 
Hmm, I can't think of a specific way to do some thing when the Screen closes.

A few ideas though.

1: You could look through the various Screens and see if any of them use and onClose methoded or a method with a name like that. Make sure it isn't something they added to their Screen class and just call manually.

2: You could check for the clicking of the Screen's Exit Button, assuming it has one, and handle it like you would any other Input.

Other then that, I can't really help you I'm afraid, I've never tried to do anything on the close of the Screen.
 
Thank you very much!

onClose() did the job.

I added this to my screen:

Code:
def onClose(self) :
     ...
     ...
     return 0

and updated the Handle Close Map in CvScreensInterface.py. :)
 
First thing first : thanks for all the work Jeckel !
It looks so simple that everyone one can change it :D

May I ask your help ?

I followed the instructions and unlike everyone here I didn't achieve my goal :/
If I get it right, you give all the instructions ( and even the files ) to change to "welcome" screen. The one right after the introduction movie of the game ( not the one with Nimoy's voice, the other one, the real first movie ), you know the one with the warrior and his sword.

When I try to change it with your step by step I still get it. Like there is no change.
What can I be doing wrong ?

Thanks,
coffee junkie
 
I haven't read it carefully, but I think the author states how to create a NEW screen which is not displayed anywhere unless you setup so (using the tutorial he gave a link to).

It has nothing to do with Main Menu screen.
 
Thanks for answering Deon. I understood the concept but I thought it could be a clue to add/customize an intro screen. :)

Still looking for ...
 
A quick way:
Go in your civ4/beyond the sword/mods/charlemagne/art/interface/main menu/ folder, copy "CIV4MainMenuBG.nif" and ""CharlemagneBG.dds" to "/'your mod'/assets/art/interface/Main Menu/", then open "CharlemagneBG.dds" in photoshop or whatever you use to edit DDS and edit the image as you like. If you don't have photoshop, I can make you a DDS out of any picture you ask.
 
Top Bottom