Zune Text Browser using the TextHandler Class

Applies to: Zune (Code works in Windows as well)

In my last tutorial, I covered building a static TextHandler class to assist in drawing text to the screen. In this tutorial, I'm going to expand on that concept by building a simple text file viewer for the Zune. First, though, some discussion about the platform and the limitations we have under it.

The Zune is a relatively restricted device (especially true compared to your Windows PC). The only (official) way to get files on or off of the Zune is via the Zune software or an XNA installer. We don't have access to browse the Zune's file system directly or store arbitrary types of files on the device.

The reason I bring this up is that the Text Viewer we will be building is really more of an example and proof of concept, since we can't actually add text files to it outside of the Visual Studio environment. All of the text files we want to view with our Zune will need to be included in our project when it is deployed to the Zune device.

That said, this tutorial will cover some interesting concepts such as:

  • Working at a basic level with the file system (Loading files and setting up to save "games" on the Zune)
  • Utilizing the TextHandler class
  • Fun String Handling!
  • Dealing with input on the Zune
  • Managing simple Game States
  • and a few other things...

Getting Started

From a design standpoint, we will create a "game" with two modes. In the first mode, we will be browsing a scrollable list of text files that have been added as content to the project before it gets deployed to the Zune. We will use the System.IO namespace's features to retrieve the list of files.

In the second mode, we will display the contents of the selected file to the screen, displaying enough lines to fill the display. The user can use the Zune Pad to scroll up and down, and use left and right on the pad to page up and down.

In both modes, pressing the "Play" button on the Zune switches to the other mode.

In order to demonstrate a few more features, we will also track the position we were in when we last viewed a file so that we can return directly to that position if we open the file again. We will do this by creating files in our save game area.

Fire up Visual Studio and begin by creating a new Zune Game project calling it "ZuneTextViewer". Use Windows Explorer to copy the TextHandler.cs into your project folder and then include it in your project (Right click on the project name in Solution Explorer, Add, Existing Item). Update the namespace in the TextHandler.cs class to match the namespace of the game project (ZuneTextViewer).

I've created a "starter" project which can be downloaded here that contains the new project with TextHandler.cs set up appropriately.

As with our demo project, we are going to need some SpriteFonts, so create a Fonts folder under Content and right click on Fonts, select Add, New Item, select SpriteFont, and add a SpriteFont called "Pescadero12.spritefont" to the project.

Edit the details of the SpriteFont file to change the FontName from Kootenay to Pescadero, and change the size to 12. Scroll down a bit further in the file and look for a line that says:

<!-- <DefaultCharacter>*</DefaultCharacter> -->

Remove the comment characters (<!-- from the beginning and --> from the end) from the line to uncomment it.

The DefaultCharacter attribute in a SpriteFont allows us to specify a character that will be used whenver we try to print a character that is not in our SpriteFont. This can happen if we convert text from a file that uses angled quote marks or the like and will prevent our viewer from crashing if the text files we view contain characters that aren't part of our default character set.

Extending TextHandler.cs

We will be adding two new methods to TextHandler.cs in order to make coding the main part of our TextHandler a little easier. These will make it easier to pull a potentially long text file apart into single lines that we can then use in our code.

The first of these methods is fairly generic, and simply returns the number of times a particular character appears in a string. We will be using this to detect newline characters in the files we process:

        // Return the number of occurances of a particular character in a string

        static public int GetCharCount(char SearchFor, string SearchIn)

        {

            return SearchIn.Length - SearchIn.Replace(SearchFor.ToString(), "").Length;

        }

All we are actually doing here is getting the length of the string we are searching, replacing the character we are looking for in the string with an empty string (thus removing it from the string) and subtracting the length of the result from the original. This will give us the count of the number of times the character appears in the string. We could, of course, have used a loop and counter variable to do the same job (and in fact I had it coded that way to begin with) but this is shorter and works just as well.

The second method we will be adding will be called in the same way that our WrapText method is called, except that instead of inserting newlines into the return string, it will return just enough characters to make up a single line, dropping the rest.

        static public string GetOneLine(string Text, string FontName, float MaxLineWidth)

        {

            // Create an array (words) with one entry for each word in the passed text string

            string[] words = Text.Split(' ');

 

            // A StringBuilder lets us add to a string and finally return the result

            StringBuilder sb = new StringBuilder();

 

            // How long is the line we are currently working on so far

            float lineWidth = 0.0f;

 

            // Store a measurement of the size of a space in the font we are using.

            float spaceWidth = Fonts[FontName].MeasureString(" ").X;

 

            // Loop through each word in the string

            foreach (string word in words)

            {

                Vector2 size;

                size = Fonts[FontName].MeasureString(word);

 

                // If this word will fit on the current line, add it and keep track

                // of how long the line has gotten.

 

                if (word.Contains("\n"))

                {

                    sb.Append(' ', GetCharCount('\n', word));

                    break;

                }

                if (lineWidth + size.X < MaxLineWidth)

                {

                    sb.Append(word + " ");

                    lineWidth += size.X + spaceWidth;

                }

                else

                // otherwise, append a newline character to start a new line.  Add the

                // word and a space, and set the size of the new line.

                {

                    break;

                }

            }

            // return the resultant string

            return sb.ToString();

        }

It is, in fact, very similar to our WrapText method. You will notice that a line can end one of two ways: we can fill up our MaxLineWidth, or we can run into a "word" that contains a newline (\n) character. In the latter case, we append a number of spaces to the string equal to the number of newline characters the word contains. This simply helps with formatting a bit.

Building the Text Viewer

That's it for our modifications to the TextHandler class. Everything else we do will now be done inside our Game class, so switch back to the Game1.cs file.

As I stated above, we will be using the System.IO namespace, so make sure you add:

using System.IO;

to the using declarations at the top of the file.

We are going to have a number of declarations that we will add right after "SpriteBatch spriteBatch;" in our game's declarations area. I'm going to paste the full list of them in here. They are all commented, but I will discuss a few of them in detail below.

 

        // List holding the names of all valid Text Files

        List<string> TextFiles = new List<string>();

 

        // Texture for the Scroll Bar

        Texture2D t2dScrollBar;

 

        // Font to use when viewing text/menu items

        string ViewFont = "Pescadero12";

 

        // Our "game" can have two modes : Menu and Reader

        enum Modes { Menu, Reader } ;

 

        // Default to starting in Menu Mode

        Modes CurrentMode = Modes.Menu;

 

        // The file we last viewed so we don't have to reload if we go back to it.

        int CurrentFile = -1;

 

        // Track where we are on the menu screen

        int MenuMode_TopFileLine = 0;

        int MenuMode_CurFileLine = 0;

 

        // A List object to break SourceText into lines that fit on the display

        // of the Zune in the selected font (Pescadero12)

        List<string> TextLines = new List<string>();

 

        // The top line of text displayed on the Zune

        int ReaderMode_CurLine = 0;

 

        // How long to delay between scrolling the screen

        float KeyPressDelay = 0.2f;

 

        // Amount of time that has passed since the screen last scrolled.

        float KeyPressTimer = 0.0f;

 

        // Lowest cursor position at which text will be printed.  This value defaults

        // to the height of the screen, but it will be modified later to take the height

        // of text lines into account.

        int CursorBottom;

 

        // The number of lines that fit on the display.  Will be calculated later

        int LinesPerPage;

 

        // The maximum width of a single line of text.

        int SingleLineWidth;

 

        // A StorageContainer where we can save a small text file containing the

        // line number we are on when we exit the program.  This allows us to return

        // to the program and start at the line we left off on when the program closed.

        StorageContainer SaveLocation;

 

When our application is launched, we will cache the names of all of the text files available for us to browse into a List object for display when the reader is in "Menu Mode". In the particular case of this application, we don't need to update this list dynamically, becase the only way to get files onto the Zune and available to our Text Reader is to add them to your XNA project before deployment.

The "TextFiles" List object will hold this list of file names and be used both for displaying the names on the menu and passed to our file loader function to actually read the file from the device.

Our next object is a Texture2D called t2dScrollBar. We will create this shortly, but essentially we will be using a 1x1 pixel white texture and taking advantage of the ability of the SpriteBatch object to colorize a texture when drawing. We'll create and load the 1x1 texture when we get to the LoadContent method.

The ViewFont object simply specifies the name of the font that we will use throughout the program to display text. You could add other fonts to the project and change this variable to customize the viewer.

I mentioned "Menu Mode" above. In fact, we will have two modes in our application: Menu Mode and Reader Mode. Our Update and Draw methods will check to see which mode the application is in and act accordingly. While we could do something like define constants where 1=Menu Mode and 2=Reader Mode, we will take a more "C#" approach, and use an enumeration.

An enumeration (or enum) is a series of potential values that basically result in our own custom type. When we define an enum containing our list of modes, we can then create variables of that type, assigning them to the modes we established. This sounds more complex than it is. As you can see above, we declared the "Modes" enum to have possible values of "Menu" and "Reader". We then created a variable called "CurrentMode" and set it to Modes.Menu, indicating that the program will start out in Menu Mode.

The remainder of the variables are mostly involved in keeping track of our place either within the menu or within the file we are viewing. I'll skip over these integer values here and address them when we use them later.

Besides those values, there is also another List object called "TextLines". We will use this when we load a text file to split the file up into single lines that we will display to the screen. It is simply a List of strings containing the line data.

Finally, if you have followed my other game tutorials, you will be familiar with the two float values calld KeyPressDelay and KeyPressTimer. We will use these to pace the input that the program receives so we aren't taking actions as fast as the game's Update loop runs.

The last of these declarations I'll talk about here is the "SaveLocation" variable, which is of type StorageContainer. This variable will be set to the location we will retrieve from the Framework where we can store "save games". In our case we will be storing information files, but the concept is the same.

Preparing for Testing on Windows

If you have done any type of "Device" development in the past, you might remember that Microsoft had a Device Emulator for Windows Mobile devices. When I first started thinking about XNA Zune development, I tried to track down the Zune Device Emulator to test my code on, since it is fairly inconvenient to deploy every single build to the device to try it out.

As it turns out, there isn't a Zune Device Emulator, but that's Ok! Since we aren't working with a completely foreign Framework like we would be in Windows Moble (after all, XNA runs on Windows and the XBox too) all we really need to do is set the screen size appropriately and we have a defacto Zune emulator for our purposes.

We are going to add a few lines to the Game1's Initialize() method to accomplish this. They aren't really necessary when running the project on the Zune, but they don't hurt anything either, so rather than use compiler directives to eliminate them when running on the Zune, I just leave them in place.

First, though, we need to make a Windows version of our project. This is actually very easy. On the Visual Studio menu bar, select the Project menu and click "Create Copy of [Project Name] for Windows". The project will be created and added to your solution.

You can make this the default execution project by right clicking on the project name (which will be called "Windows copy of [Project Name]") and select "Set as Startup Project".

So, back to our Initialize() method. Add the following before the call to base.Initialize() :

            graphics.PreferredBackBufferWidth = 240;

            graphics.PreferredBackBufferHeight = 320;

            graphics.ApplyChanges();


There is really nothing special here... this is the standard way we would set our desired resolution in XNA. We are just setting the backbuffer to match the size of the Zune display.

Helper Methods

There are a few things we need to be able to do during our project's initalization, and while we could simply pile all of the code into the LoadContent() method, I've split the functions out to helper methods to keep things more organized. It also allows for smaller chunks of code for discussion.

Here is the first one:

        // Establish some initial parameters about the way we will display

        // text on the screen.

        private void DetermineScreenSettings()

        {

            int ViewHeight = graphics.GraphicsDevice.Viewport.Height;

 

            // We won't start a new line of text below the point at which

            // one would entirely fit on the screen.

            CursorBottom = ViewHeight - (int)TextHandler.Fonts[ViewFont].MeasureString("X").Y;

 

            // Calculate the number of lines that can be displayed on

            // a single screen.

            LinesPerPage = (ViewHeight / (ViewHeight - CursorBottom) - 1);

 

            // Set the size of a single line to the width of the screen minus

            // 10 pixels.  We will use these 10 pixels to draw a "scroll bar" on

            // the right side of the screen.

            SingleLineWidth = graphics.GraphicsDevice.Viewport.Width - 10;

        }

You will remember that I said in the declaration section that I was going to talk about some of the Integer values later on. Well, here is the first set.

All we are doing here is determining some basic properties of the screen we will be displaying to. We will need this information when determining how much text to put on a single line, or how many lines to put onto the screen.

We can retrieve the size of the display from the Viewport object. As you can see above, we use the height of the viewport to base our CursorBottom and LinesPerPage variables on, and the Width of the viewport to determine the width of a line of text.

The "CursorBottom" variable is what we will consider the lowest point on the screen to start drawing a new line of text. Any further down and the line will be partially cut off the display.

You may also see that we reserve 10 pixels on the right side of the screen for the "scroll bar" we will add later.

Our next helper method will let us find out what files there are to view on the device:

        // Since we can't add files at "run time" we will cache a list of all of the

        // text files we have deployed with the application.

        private void CacheFileNames()

        {

            TextFiles.Clear();

 

            // Get a list of all of the text files located in the TEXT folder of content.

            DirectoryInfo info = new DirectoryInfo(StorageContainer.TitleLocation + @"\Content\Text");

 

            // Add each file to the TextFiles list

            foreach (FileInfo file in info.GetFiles())

            {

                TextFiles.Add(file.Name);

            }

        }

The first thing we do here is clear out the TextFiles List object. Strictly speaking this should not be necessary, since creating an instance of the List object results in an empty list. However, it is always good practice to make absolutely sure you are in a known state before doing anything with an object, so we'll clear it out (after all, in a program under windows we might have a way to change folders, in which case we could call this method multiple times).

Next we create a DirectoryInfo object and read the contents of the diretory indicated by "StorageContainer.TitleLocation" with "\Content\Text" appended to it. The StorageContainer.TitleLocation string is provided by XNA and points to where in the filesystem the application is installed to/running from. The result of this is that we get a DirectoryInfo object containing the directory where we will be storing our Text Files.

Finally we use a simple foreach loop to add all of the files in the directory to the TextFiles list.

        // We will be using "SaveLocation" to place marker files that

        // hold what the last view we viewed and where we were in that

        // file are.  In order to do this, we need to use the Guide to get

        // the StorageDevice.  On the Zune, there is nothing actually

        // shown when BeginShowStorageDevice is called.  Instead, it

        // immediately returns the result via EndShowStorageDeviceSelector.

        private void FindStorageDevice()

        {

            // "Show" the StorageDeviceSelector

            IAsyncResult result = Guide.BeginShowStorageDeviceSelector(null, null);

 

            // Retrieve the result (note- Nothing is shown to the user on the Zune)

            StorageDevice device = Guide.EndShowStorageDeviceSelector(result);

 

            // If we found a device (and we always will) save it to our

            // "SaveLocation" variable.

            if (device.IsConnected)

            {

                SaveLocation = device.OpenContainer("Zune XNA-Book Reader");

            }

        }

This next method uses the XNA "Guide" object to find out where we can save files at runtime. On Windows and the Zune, executing BeginShowStorageDeviceSelector doesn't actually do anything that the user needs to interact with, and we can use a shortcut method of handling the result (which is returned immediately with a call to EndShowStorageDeviceSelector. This wouldn't work the same way on the XBox, however, since the user would be prompted to select the storage device (ie, disk drive or memory module) they want to use to store the game's save data. You would actually have to handle the asynchronous nature of the call on the XBox.

On the Zune, however, you simply get back a directory name that we can use. We call the device's "OpenContainer()" method and pass it the name we wish to use for our application. The result is a StorageDevice object that we save into our "SaveLocation" variable. We will use this later when we want to save data about where the user left off when reading a text file.

Our next two helper funcitons deal with saving file to and reading files from the location we just received via the FindStorageDevice() call. We want to be able to save the current line we are viewing in each text file so that when we reload that text file we return to the same place. In order to do that, we will need a way to save the information, and a way to reload it, so we have the following to functions (I'm putting them together since they handle the same files):

 

        private void GetSavedLastLine(string CheckFileName)

        {

            if (File.Exists(SaveLocation.Path + @"\LastLoc_" + CheckFileName + ".txt"))

            {

                StreamReader sr = File.OpenText(SaveLocation.Path + @"\LastLoc_" + CheckFileName + ".txt");

                ReaderMode_CurLine = (int)MathHelper.Clamp(int.Parse(sr.ReadLine()), 0, TextLines.Count - 1);

                sr.Close();

            }

            else

            {

                ReaderMode_CurLine = 0;

            }

        }

 

        private void SaveCurrentLine(string FileName)

        {

            StreamWriter sw = File.CreateText(SaveLocation.Path + @"\LastLoc_" + FileName + ".txt");

            sw.WriteLine(ReaderMode_CurLine.ToString());

            sw.Close();

        }

In the GetSavedLastLine() function we check to see if a file called "LastLoc_" plus the filename for the file we are loading exists. If it does, we use a StreamReader object to read the line number from the file. We use MathHelper.Clamp to make sure that the value we get back is at least within the limits of the text file we are reading.

The SaveCurrentLine() function simply does the opposite. We create the text file and write the current line number to it.

Loading Text Files

We've done a lot of background work, so lets get on to actually loading a text file into our TextLines list object. There is actually only a few lines of code in this function, but a lot of comments to clarify what is going on.

Essentially, we will open a text file and then use the GetOneLine() method of the TextHandler class that we added earlier to break it into individual lines. We simply keep processing the remainder of the file until we run out of text to handle.

 

        // Called when a text files is selected from the menu.  This function

        // will load the requested text file and parse it into lines for

        // the display code to use.

        private void LoadTextFile(string FileName)

        {

            // Clear the TextLines list.

            TextLines.Clear();

 

            // Read the entire file into a string.  We will then pull it apart.  First,

            // though we will replace and carrage returns (\r) with nothing (removing them)

            // and replace new line characters (\n) with the same character with a space

            // in front of it.  This will make splitting our lines easier as a space marks

            // the end of a word.

            string WorkText = File.OpenText(StorageContainer.TitleLocation +

              @"\Content\Text\" +

              FileName).ReadToEnd().Replace("\r", "").Replace("\n", " \n");

 

            // Build the List object of lines from the text file.  In our case, the lines

            // are enough text to fit on a display line on the screen.

            do

            {

                // Extract a single line of text.  We only consider enough characters from

                // the file equal to the width of the line in pixels to accomodate lines that

                // do not have a space in them up to the point where we would break the line.

                string thisline = TextHandler.GetOneLine(WorkText.Substring(

                    0, Math.Min(WorkText.Length, SingleLineWidth)),

                    ViewFont, SingleLineWidth);

 

                // If we didn't get any characters back from the above, it indicates that there

                // is no breaking space in the line that would allow it to fit on a line

                // on the screen (ie, a long series of dashes, for example)

                // In this case, we will simply take all of the characters up to the

                // next available space.  They will get chopped off on the display.

                if (thisline.Length == 0)

                { thisline = WorkText.Substring(0, WorkText.IndexOf(' ')); }

 

                // If this line is less than the length of the remainder of the file,

                // we remove it from the string containing the text file so we start

                // with a smaller string next time through the loop

                if (thisline.Length <= WorkText.Length)

                    WorkText = WorkText.Substring(thisline.Length);

                else

                    WorkText = "";

 

                // Add this line, removing any newline characters, to the lines list

                TextLines.Add(thisline.Replace("\n", " "));

            } while (WorkText != "");

 

            GetSavedLastLine(FileName);

        }

Most everthing that I would explain here is covered by the comments in this code, so take a look at them. The most important things here are the handling of the special characers \r and \n. These characters represent a Carriage Return (\r) and a New Line (\n). We are really only interested in the New Line characters, so we strip out all \r characters from the outset.

When we hit a New Line, it is important to review hour our TextHandler methods will look at it. Recall that in our GetOneLine() method we treat a "word" containing a new line as a special case. we append a number of spaces equal to the number of \n characters in the word and drop the word completely.

This means that we can't have an actual word that ends in a \n character, which is going to be pretty common in text. Lets say we take that last sentance the way it looks in a text file : "... pretty common in text.\n". Our "text." would be dropped because it is part of the "word" that contains a newline.

So instead, we replace the "\n" character with a " \n" (a space before the \n) so that \n will always start a new word. The reason we replace the \n characters with spaces in the return value is that we check the length of the return value to determine how many characters to remove from the front of our work string before we continue processing.

The last thing we do in the LoadTextFile() method is call the GetSavedLastLine() method we added above to see if we should start somewhere other than the top of the file.

Lets LoadContent()!

Now that we have all of our Helper Methods in place, lets take a look at LoadContent(). Because we split almost everything out into helper functions, LoadContent() is going to be very simple, but we need to add a texture to our project first.

Normally at this point in my tutorials, I would create a sample texture and upload it for you to download and include in your project. I'm not going to do that this time. Lets put those art skills to work!

Right-click on Content in Solution Explorer and add a new folder called "Textures". Then, right click on Textures and click Add, New Item. In the Add New Item window that appears, select "Bitmap File" and call it ScrollBar.bmp.

When you do this, you will get an edit that looks a lot like the standard Windows "Paint" program, zoomed really far into a 48x48 pixel bitmap. In the Properties window for the new bitmap object, set both the Height and the Width to 1 (if you don't see a Height and Width property, you are probably seeing the Content Manager related properties. Double-click on the "ScrollBar.bmp" file name in Solution Explorer and the properties should change to the file properties.

Save your ScrollBar.bmp file and close it. Yes, we are going to be using a 1x1 pixel white bitmap as our scroll bar image. Even *my* art skills are up to that challenge, and I can barely draw a straight line.

Back in our Game1.cs file, lets set up our LoadContent() method to initialize the TextHandler, load our bitmap, and call our helper methods to get things set up:

 

        protected override void LoadContent()

        {

            // Create a new SpriteBatch, which can be used to draw textures.

            spriteBatch = new SpriteBatch(GraphicsDevice);

 

            t2dScrollBar = Content.Load<Texture2D>(@"Textures/ScrollBar");

 

            // Initialize the TextHandler class

            TextHandler.Initialize(spriteBatch, Content, "Fonts");

 

            // Set up the Screen Size and Lines Per Page

            DetermineScreenSettings();

 

            CacheFileNames();

 

            FindStorageDevice();

 

            // TODO: use this.Content to load your game content here

        }

Again, since we broke our actual setup tasks down to their own methods, LoadContent() becomes pretty straightforward.

The Update() Method

It is finally time to handle some user input. (I know, I know... we *still* haven't displayed anything onto the screen yet!). Our control scheme is fairly simple. We have two modes (or Game States if you want to call them that). Our controls are similar in both modes : The user can scroll up or down, page up or down, change between modes, or exit the program.

We will handle all of the input at one time, determining the command the user wants to execute, and then process that "command" based on the mode we are currently in.

The Update() method is a big chunk of code, but it is also fairly simple. We start by reading the GamePad and Keyboard states and storing them. We then check for various button/key presses and, based on what is pressed, determine a command to process.

Then it is simply a matter of translating the command into what we want to happen on the screen for each mode. Here is the Update() method itself. I'll take a bit more about it after the code.

 

        protected override void Update(GameTime gameTime)

        {

            // Start with no command to perform this cycle

            string CurrentCommand = "NONE";

 

            GamePadState gs = GamePad.GetState(PlayerIndex.One);

            KeyboardState ks = Keyboard.GetState();

 

            // Pressing "Back" on the Zune (Win: Esc) exits the reader

            if ((gs.Buttons.Back == ButtonState.Pressed) ||

                (ks.IsKeyDown(Keys.Escape)))

            {

                CurrentCommand = "EXIT";

            }

 

            // Check for Zune-Pad "Touches".  We can scroll one line at a

            // time up or down by touching the Zune Pad (paging requires a

            // "Click" of the left/right DPad)

            if (gs.ThumbSticks.Left.Y > 0.25)

                CurrentCommand = "UP";

            if (gs.ThumbSticks.Left.Y < -0.25)

                CurrentCommand = "DOWN";

 

            // Handle the DPad (for earlier Zunes and for paging on new zunes)

            // Since these are handles AFTER the Zune Pad Touches, if the user

            // is actually clicking left or right, the Touch command that may

            // have been triggered by being a little high or low will be overridden.

            if (gs.DPad.Up == ButtonState.Pressed)

                CurrentCommand = "UP";

            if (gs.DPad.Down == ButtonState.Pressed)

                CurrentCommand = "DOWN";

            if (gs.DPad.Left == ButtonState.Pressed)

                CurrentCommand = "PAGEUP";

            if (gs.DPad.Right == ButtonState.Pressed)

                CurrentCommand = "PAGEDOWN";

 

            // Handle the Keyboard (For Windows-based testing)

            if (ks.IsKeyDown(Keys.Up))

                CurrentCommand = "UP";

            if (ks.IsKeyDown(Keys.Down))

                CurrentCommand = "DOWN";

            if (ks.IsKeyDown(Keys.Left))

                CurrentCommand = "PAGEUP";

            if (ks.IsKeyDown(Keys.Right))

                CurrentCommand = "PAGEDOWN";

 

            // On the Zune, the "Play" button toggles a mode switch between

            // Reader and Men modes.  Windows uses the Space Bar or Enter key.

            if (gs.Buttons.B == ButtonState.Pressed)

                CurrentCommand = "MODESWITCH";

            if (ks.IsKeyDown(Keys.Enter) || ks.IsKeyDown(Keys.Space))

                CurrentCommand = "MODESWITCH";

 

            KeyPressTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;

 

            // Process commands while in Reader Mode.

            if (CurrentMode == Modes.Reader)

            {

                // Make sure enough time has passed to repeat a command

                if (KeyPressTimer >= KeyPressDelay)

                {

                    switch (CurrentCommand)

                    {

                        // Save the current line position for this file and exit.

                        case "EXIT":

                            SaveCurrentLine(TextFiles[CurrentFile]);

                            this.Exit();

                            break;

 

                        // UP and DOWN decrement or increment the ReaderMode_CurLine

                        // variable by 1, clamping it to a valid range.

                        case "UP":

                            ReaderMode_CurLine = (int)MathHelper.Clamp(ReaderMode_CurLine - 1, 0, TextLines.Count - LinesPerPage);

                            break;

                        case "DOWN":

                            ReaderMode_CurLine = (int)MathHelper.Clamp(ReaderMode_CurLine + 1, 0, TextLines.Count - LinesPerPage);

                            break;

 

                        // PAGEUP and PAGEDOWN decrement or increment the ReaderMode_CurLine

                        // variable by the number of lines per page, clamping it to valid values.

                        case "PAGEUP":

                            ReaderMode_CurLine = (int)MathHelper.Clamp(ReaderMode_CurLine - LinesPerPage, 0, TextLines.Count - LinesPerPage);

                            break;

                        case "PAGEDOWN":

                            ReaderMode_CurLine = (int)MathHelper.Clamp(ReaderMode_CurLine + LinesPerPage, 0, TextLines.Count - LinesPerPage);

                            break;

 

                        // The MODESWITCH command saves the current line in this file

                        // and switches to Menu mode.

                        case "MODESWITCH":

                            SaveCurrentLine(TextFiles[CurrentFile]);

                            CurrentMode = Modes.Menu;

                            break;

                        default:

                            break;

                    }

 

                    // Reset the delay timer to keep the command from repeating too fast.

                    if (CurrentCommand != "NONE")

                        KeyPressTimer = 0.0f;

 

                }

            }

            else

            {

                // Menu Mode

 

                if (KeyPressTimer >= KeyPressDelay)

                {

                    switch (CurrentCommand)

                    {

                        // No need to save a file location since we aren't currently

                        // viewing a file, so just exit.

                        case "EXIT":

                            this.Exit();

                            break;

 

                        // UP and DOWN scroll through the menu by 1 item.

                        case "UP":

                            MenuMode_CurFileLine = (int)MathHelper.Clamp(MenuMode_CurFileLine - 1, 0, TextFiles.Count - 1);

                            break;

                        case "DOWN":

                            MenuMode_CurFileLine = (int)MathHelper.Clamp(MenuMode_CurFileLine + 1, 0, TextFiles.Count - 1);

                            break;

 

                        // PAGEUP and PAGEDOWN scroll through the menu by LinesPerPage items.

                        case "PAGEUP":

                            MenuMode_CurFileLine = (int)MathHelper.Clamp(MenuMode_CurFileLine - LinesPerPage, 0, TextFiles.Count - 1);

                            break;

                        case "PAGEDOWN":

                            MenuMode_CurFileLine = (int)MathHelper.Clamp(MenuMode_CurFileLine + LinesPerPage, 0, TextFiles.Count - 1);

                            break;

 

                        // MODESWITCH loads the selected file (if it wasn't the one we

                        // were already viewing) and switches to Reader Mode.

                        case "MODESWITCH":

                            if (CurrentFile != MenuMode_CurFileLine)

                            {

                                CurrentFile = MenuMode_CurFileLine;

                                LoadTextFile(TextFiles[MenuMode_CurFileLine]);

                            }

                            CurrentMode = Modes.Reader;

                            break;

                        default:

                            break;

                    }

 

                    // Reset the delay timer to keep the command from repeating too fast.

                    if (CurrentCommand != "NONE")

                        KeyPressTimer = 0.0f;

                }

 

                // Handle scrolling the menu up or down if necessary.

                if (MenuMode_CurFileLine < MenuMode_TopFileLine)

                    MenuMode_TopFileLine = MenuMode_CurFileLine;

 

                if (MenuMode_CurFileLine > (MenuMode_TopFileLine + LinesPerPage))

                    MenuMode_TopFileLine = MenuMode_CurFileLine - LinesPerPage;

 

            }

 

            // If no command was entered (and therefore no button was pressed) boost up

            // the delay timer so that a user can press, release, and press again without

            // being limited by the delay timer.

            if (CurrentCommand == "NONE")

                KeyPressTimer = KeyPressDelay + 1.0f;

 

            // TODO: Add your update logic here

 

            base.Update(gameTime);

        }

 

One thing to note is the handling of the "NONE" command. Whenever we process a "NONE" command, we set KeyPressTimer to KeyPressDelay + 1, ensuring that the next time we process a command KeyPressTimer will be greater than KeyPressDelay.

Why? Well, we have the delay in there to prevent the screen from scrolling as fast as the Zune can process the Update event as soon as you touch the Zune Pad. But what if the user WANTS to scroll faster than the built-in delay? By writing it this way, we can allow the user to rapidly "tap" their commands. Each time they release the button the KeyPressTimer will "reset" allowing the next press to be detected immediately.

The Draw() Method

We are finally there. We have all the background work we need so all that is left is to display the results of our efforts to the screen.

Just like our Update() method, we will be handling two different modes (or States) with our Draw() method. When CurrentMode is equal to Modes.Reader we will be displaying lines from the current text file. Otherwise we will be displaying the list of text files to select from.

I'm going to paste the whole method here, and then talk about the interesting bits below.

 

        protected override void Draw(GameTime gameTime)

        {

            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin(SpriteBlendMode.AlphaBlend);

 

            // Specify our TextHandler starting conditions.

            TextHandler.CurrentFont = ViewFont;

            TextHandler.CurrentColor = Color.White;

            TextHandler.CursorLocation = Vector2.Zero;

 

            // Reader Mode

            if (CurrentMode == Modes.Reader)

            {

                // Begin with the current line...

                int ThisLine = ReaderMode_CurLine;

 

                // Loop untip we reach the bottom of the screen

                while (TextHandler.CursorLocation.Y < CursorBottom)

                {

                    // Make sure the line we are going to print exists in

                    // the line list.

                    if (ThisLine < TextLines.Count)

                    {

                        // Print the line to the screen and increment ThisLine

                        TextHandler.Print(TextLines[ThisLine++]);

                    }

                    else

                    {

                        // If it doesn't, just print a blank line

                        TextHandler.Print(" ");

                    }

                }

 

                // Draw the "Scroll Bar"

 

                // First, Draw a yellow "line" the entire height of the screen

                spriteBatch.Draw(t2dScrollBar,

                    new Rectangle(GraphicsDevice.Viewport.Width - 3, 0, 1,

                        GraphicsDevice.Viewport.Height),

                    Color.Yellow);

 

                // Determine where the file position marker should start,

                // based on the current line in the file versus the number

                // of total lines.

                int MarkerPos = (int)(

                    (float)GraphicsDevice.Viewport.Height *

                    ((float)ReaderMode_CurLine / (float)TextLines.Count));

 

                // Determine the height of the file position marker.  Short

                // files will have larger markers.

                int MarkerHeight = (int)(

                    ((float)LinesPerPage / (float)TextLines.Count) *

                    GraphicsDevice.Viewport.Height);

 

                // Make sure we have at least 3 pixels on the marker.

                if (MarkerHeight < 3)

                    MarkerHeight = 3;

 

                // Draw the Marker

                spriteBatch.Draw(t2dScrollBar,

                    new Rectangle(

                        GraphicsDevice.Viewport.Width - 3,

                        (int)MarkerPos,

                        1,

                        MarkerHeight),

                    Color.Red);

            }

            else

            // Menu Mode

            {

                // Starting with "MenuMode_TopFileLine", loop until we either

                // pass the total number of text files, or until we run out of

                // room on the screen.

                for (int x = MenuMode_TopFileLine;

                    x <= (MathHelper.Min(MenuMode_TopFileLine + LinesPerPage,

                    TextFiles.Count - 1)); x++)

                {

                    // If the item we are about to draw is the currently selected

                    // menu item, change our drawing color to LightGreen

                    if (x == MenuMode_CurFileLine)

                    {

                        TextHandler.CurrentColor = Color.LightGreen;

                    }

                    else

                    // Otherwise, we'll draw in White.

                    {

                        TextHandler.CurrentColor = Color.White;

                    }

                    // Use TextHandler to print the file name for this menu item.

                    TextHandler.Print(TextFiles[x]);

                }

            }

We start by clearing the display to a black screen, and then start a SpriteBatch.Begin() call.

Next, we need to set our defaults for the TextHandler class for this interation of the Draw() method. We want to set our font, set the default drawing color to white, and position our virtual "cursor" at the upper left of the display.

At this point, we split depending on which mode we are in. In Reader mode, we start a simple While loop that will continue running for each "line" on the display (there are actually several ways we could have done this... a for loop from 1 to LinesPerPage would also work, but I wanted to use the CursorLocation check just to make some more use of the TextHandler.)

For each line we are going to print, we check to see of the line exists in our TextLines list. If it does, we use TextHandler's Print() method (which will take care of advancing the cursor to the next line for us) to display the line.

If we have gone off of the end of the text file, we simply print a blank space. Again, we could also just do nothing here and accomplish the same result if we were using a for loop, but since we are looping until we run out of display room, we need to print the blank line so the cursor moves down.

Our next step in Reader mode is to draw the scroll bar. Our 1x1 pixel white bitmap is going to do some extra duty for us here thanks to a few features of the SpriteBatch.Draw() method.

In our first spriteBatch.Draw() call we draw a yellow line from the top of the display to the bottom of the display along the right side of the screen. we can do this because when we specify the destination rectangle for the sprite to be drawn, it doesn't have to be the same size as the source (1x1 pixel) rectangle. SpriteBatch will stretch the image out over the destination area.

We can also use the last parameter of SpriteBatch.Draw() to tint our sprite. Since our bitmap is white, any color we specify in the last parameter will be the color for the whole "texture" when it is displayed.

The math comes next. We have to figure out how far down on the scroll bar to draw the position indicator, and how big to make it. The code looks more complicated than it is because of all of the type casting involved (we can't divide two integers and get a float, so we have to (float) both ReaderMode_CurrentLine and TextLines.Count and divide one by the other to get a percentage (ie, if we are at line 0 of 100, 0/100=0. If we are at line 50 of 100, 50/100 = 0.5).

We multiply this percentage by the height of the screen, resulting in the number of pixels from the top we will start drawing our marker block (after finally converting all of those (float)s back to an (int)).

The process to determine the height of the marker is similar. We divide LinesPerPage by TextLines.Count, giving the percentage of the file that a single page consist of (ie, if 10 lines fit on the screen and we have a 100 line file, 10/100=0.1). We multiply this by the height of the display to get the height of a single page relative to the height of the whole file (in this case, 320 * 0.1 = 32).

Lastly, we make sure that the marker is going to be at least 3 pixels high just so we can actually see it on long files.

All that is left to do now is to draw it. Again, we will be stretching our sprite over the required area and colorizing it, this time with Color.Red.

That handles Reader Mode, so the next thing to deal with is Menu Mode.

This time around (just for variety) we are going to use a for loop. We will loop starting at MenuMode_TopFileLine, and continue until we either run out of space on the screen, or reach the last item in the TextFiles list.

The only decision we have to make here is what color to draw the item in. If it is our "current" menu item (meaning that MenuMode_CurFileLine is equal to this item) we want to draw the item in Color.LightGreen to show it as hilighted. Otherwise, we will draw it with Color.White.

After we decide on a color, we simply use TextHandler.Print to print the entry and advance the cursor.

Add Some Files

The last thing to do is add some text files to view. Right click on Content once again, and add a new folder called Text. Now browse around and find some interesting things to put on your Zune as text files. Add each one of them to the Text folder and include them in your project.

In the Properties window, set the Build Action to "None" and set Copy to Output Directory to "Copy Always". We need to make these changes since we don't want Content Manager to try to process these items.

Fire It Up!

You should now have a working (though admittedly of limited use since you can't add files to the Zune outside of Visual Studio and redeploying your application) text file browser.

Going in and out of text files should store your current position so that when you return to that file later you start out at the same point.

Ideas for Expansion

As I have already said, due to the limitations of what we can actually place on the Zune, this application has a fairly limited usefulness potential. That said, there are a couple of ways you could make it fancier:

  • Include a way to change fonts. As written, this would require rereading the current file since the file is split into lines when it is loaded based on the current font.
  • Add the ability to rotate the display and read in "landscape" mode.

Here is my final project folder with a few text files that you may find familiar included:

Download Source

































 
 
 
Site Contents Copyright © 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED