Click here to Skip to main content
Click here to Skip to main content

TabText

, 10 Jul 2006
Rate this:
Please Sign up or sign in to vote.
TabText is a text-editor with tab-pages, like Excel.

Introduction

TabText is a text-editor with tab-pages, like Excel.

The program has the capabilities of the following extensions, which have been published on The Code Project:

namespace Khendys.Controls
{
    public class ExRichTextBox : RichTextBox
    {
        //code of Khendis Gordon
    }
}

using Khendys.Controls;
namespace Nik.UserControls
{
    public class RicherTextBox2 : ExRichTextBox
    {
        //code of Nikola Stepan
    }
}

using Nik.UserControls;
namespace MicrosoftMSDN
{
    public class RichTextBoxExt : RicherTextBox2
    {
        //Code of MSND
    }
}

using MicrosoftMSDN;
namespace Compenkie
{
    public class RichTextBoxExtend : RichTextBoxExt
    {
        //code of myself
    }
}

TabText is extended with:

  • tab-pages, so you can easily switch between documents.
  • added page setting of the PageSetupDialog to every page.
  • serializes all the pages in one file.
  • recent-file-list through an array of ToolStripMenuItems.
  • added a simple table. The table use the functionality of the RichTextBox to add more rows in the table, by using the Enter-key.
  • adds functions for Find & Replace as described in the MSDN Library.
  • inserts emoticons through a picture panel.
  • inserts text fragments through a context menu.
  • help pages.
  • possibility to change the language.
  • keyboard state.
  • store the settings of the program in the registry.

TabText makes a folder, My Documents\\Compenkie\\TabText1, to store and load data files. TabText makes a Registry key, CurrentUser\\Compenkie\Tabext1, to store settings.

The construction of the program is as follows:

Black    = Form                 = TabText
Red    = SplitContainer              = splitContaimer
Bleu    = TabControl             = tabControl
Green    = RichTextBoxExtended (left)    = richTextBox
Green    = RichTextBox (right)        = richTextBoxPreview

The richTextBoxPreview is only to preview the text fragments.

Tab-Pages

Pages are made dynamic in the same way as the designer does. At every page is a rich-text box added, of type RichTextBoxExtended.

public RichTextBoxExtend[] richTextBox = new RichTextBoxExtend[17];
for (int teller = 0; teller < 17; teller++)
{
    richTextBox[teller] = new RichTextBoxExtend();
    // 
    // richTextBox1
    // 
    richTextBox[teller].AcceptsTab = true;
    richTextBox[teller].Dock = System.Windows.Forms.DockStyle.Fill;
    richTextBox[teller].Location = new System.Drawing.Point(3, 3);
    richTextBox[teller].Name = "richTextBox" + teller.ToString();
    richTextBox[teller].Size = new System.Drawing.Size(397, 380);
    richTextBox[teller].TabIndex = teller;
    richTextBox[teller].TabStop = true;
    richTextBox[teller].Text = "";
    richTextBox[teller].SelectionChanged += 
                new EventHandler(TabText_SelectionChanged);
    richTextBox[teller].CursorPositionChanged += 
                new EventHandler(TabText_CursorPositionChanged);
    richTextBox[teller].AllowDrop = false;
    richTextBox[teller].DragDrop += new DragEventHandler(TabText_DragDrop);
    richTextBox[teller].DragEnter += new DragEventHandler(TabText_DragEnter);
}

TabPage[] tabPages = new TabPage[17];
public void AddPage(int pagenummer)
{
    // tabPage
    //

    tabPages[pagenummer] = new TabPage();
    tabPages[pagenummer].Location = new System.Drawing.Point(4, 22);
    tabPages[pagenummer].Name = "tabPage" + pagenummer.ToString();
    tabPages[pagenummer].Padding = new System.Windows.Forms.Padding(3);
    tabPages[pagenummer].Size = new System.Drawing.Size(403, 386);
    tabPages[pagenummer].TabIndex = pagenummer;
    tabPages[pagenummer].MouseHover += new EventHandler(TabText_MouseHover);
    tabPages[pagenummer].MouseLeave += new EventHandler(TabText_MouseLeave);
    tabPages[pagenummer].ToolTipText = "Dubbelklikken voor wijzigen naam tab";
    tabPages[pagenummer].Text = "tabPage" + pagenummer.ToString();
    tabPages[pagenummer].Tag = documentDir + "\\" + 
                               tabPages[pagenummer].Text + ".rtf";
    tabPages[pagenummer].UseVisualStyleBackColor = true;
    tabPages[pagenummer].Controls.Add(richTextBox[pagenummer]);
    tabControl.Controls.Add(tabPages[pagenummer]);
    tabPages[pagenummer].SuspendLayout();
    tabPages[pagenummer].ResumeLayout(false);
    int aantal = tabControl.Controls.Count;
    tabControl.SelectedIndex = aantal - 1;
    this.Text = "TabText - " + tabPages[pagenummer].Text;
}

The pages are named "TapPage0", "Tabpage1", ... In the Text property of the tap-page is the filename. In the Tag property of the tap-page is the full path of the file. With a double-click on the tab, appears a textbox where you can change the filename. The rich-text box turns off DockStyle.Fill so you can make space on the top of the tab control to show a TextBox.

private void tabControl_DoubleClick(object sender, EventArgs e)
{
    TabControl conTrol = (TabControl)sender;
    index = conTrol.SelectedIndex;
    richTextBox[index].Dock = DockStyle.None;
    richTextBox[index].Location = new Point(0, 25);
    richTextBox[index].Size = new Size(this.Size.Width, Size.Height - 25);
    box = new TextBox();
    box.Leave += new System.EventHandler(this.box_Leave);
    box.Text = tabPages[index].Text.ToString();
    tabPages[index].Controls.Add(box);
    box.Show();
    box.Focus();
}

If the focus leaves the textbox then the name will add to the text of the tab-page. If you do not give an extension then the standard extension rtf will be added. The full-name of the file will be added to the Tag property of the tab-control.

private void box_Leave(object sender, EventArgs e)
{
    TextBox box = (TextBox)sender;
    tabPages[index].Text = box.Text.ToString();
    if (Path.GetExtension(tabPages[index].Text.ToString()) == "")
        tabPages[index].Tag = documentDir + "\\" + 
                              tabPages[index].Text + ".rtf";
    else
        tabPages[index].Tag = documentDir + "\\" + tabPages[index].Text;
    box.Hide();
    richTextBox[index].Dock = DockStyle.Fill;
}

Load File

LoadFile will be accessed by:

  • the menu-item Load.
  • the recent-file menu-item Load.

The Modified property will be initially set to false. By editing the rich-text box, the property becomes true. This will be used in the Save routine. The full name of the file will be stored in the Tag property, and the file will be add to the recent file list. If present, then first remove and then add. If the the extension is RTF, then load as an RTF-file, in all other cases, as a text-file.

private void loadFile(string fileName)
{
    TabPage tab = tabControl.SelectedTab;
    try
    {
         int index = ZoekTab();
         string extentie = Path.GetExtension(fileName);
         if (extentie == ".rtf")
             richTextBox[index].LoadFile(fileName, 
                     RichTextBoxStreamType.RichText);
         else
             richTextBox[index].LoadFile(fileName, 
                     RichTextBoxStreamType.PlainText);
         richTextBox[index].Modified = false;
         tab.Tag = fileName; ;
         tab.Text = Path.GetFileName(fileName);
         this.Text = "TabText - " + tab.Text;
         // add successfully opened file to recent file list
         removeRecentFile(fileName);
         addRecentFile(fileName);
    }
    catch (IOException ex)
    {
         removeRecentFile(fileName);
                Trace.WriteLine(ex.Message, "Error loading from file");
    }
}

Save File

The file is normally saved as TXT or RTF, depending on the extension of the filename. The default is "rtf". If a file is edited, you will be asked if you want to save it.

private void saveFile(TabPage tab)
{
    int index = ZoekTab();
    if (richTextBox[index].Modified == true)
    {
        string messageString = "Save file " + tab.Text.ToString() + "?";
        if (MessageBox.Show(messageString, "TabText1", 
            MessageBoxButtons.YesNo) == DialogResult.Yes)
        {
            string directorie = Path.GetDirectoryName(tab.Tag.ToString());
            if (Directory.Exists(directorie) == false)
                        Directory.CreateDirectory(directorie);
            string extentie = Path.GetExtension(tab.Tag.ToString());

            if (extentie == ".rtf")
                richTextBox[index].SaveFile(tab.Tag.ToString(), 
                               RichTextBoxStreamType.RichText);
            else
                richTextBox[index].SaveFile(tab.Tag.ToString(), 
                              RichTextBoxStreamType.PlainText);
         richTextBox[index].Modified = false;
        }
    }
    addRecentFile(tab.Tag.ToString());
}

Recent File List

The recent file-list is made by an array of ToolStripMenuItems.

private ToolStripMenuItem[] recentFiles = new ToolStripMenuItem[11];
int maxRecent;

In the variable maxRecent is the actual number of recent files stored.

In the Load event of the form, the array will be filled. In the property ToolTipText is the path of the file stored, so we will have a nice tooltip.

RegistryKey key = Registry.CurrentUser.OpenSubKey(strRegKey + 
                                       "Recent File List\\");
if (key != null)
{
    try
    {
        for (maxRecent = 0; maxRecent < 10; maxRecent++)
        {
             string sKey = "file" + maxRecent.ToString();
            string longfileNaam = (string)key.GetValue(sKey, "");
            if (longfileNaam.Length == 0)
                break;
            recentFiles[maxRecent].ToolTipText = 
                                longfileNaam.ToString();
            recentFiles[maxRecent].Text = 
                  GetShortDisplayName(longfileNaam, 40);
        }
    }
    catch (Exception ex)
    {
        Trace.WriteLine("Loading Recent Files" + 
                        " from Registry failed: " + ex.Message);
    }
        key.Close();
}

if (recentFiles[0].Text != "")
        loadFile(recentFiles[0].ToolTipText.ToString());

If a file is present initially, it will be opened and a tap-page will be made.

Serialization

With serialization, all the tab-pages are stored in one file. A BinaryFormatter will be used to store data-types. For every page, we store the file-name and the full path-name of the file. Also will be saved the page settings of the PageSetupDialog for every TabPage. After selecting the complete content of the rich-text box, save the content. With a for-loop, we will then save all the pages.

private void serializeFileDialog_FileOk(object sender, CancelEventArgs e)
{
    sbpMenu.Text = "Serialize alle pagina's";
    Stream stream = File.Open(serializeFileDialog.FileName, FileMode.Create);
    BinaryFormatter bformatter = new BinaryFormatter();
    bformatter.Serialize(stream, maxTab);
    for (int teller = 0; teller < maxTab; teller++)
    {
         bformatter.Serialize(stream, richTextBox[teller].printLandScape);
         bformatter.Serialize(stream, richTextBox[teller].printMarginTop);
         bformatter.Serialize(stream, richTextBox[teller].printMarginLeft);
         bformatter.Serialize(stream, richTextBox[teller].printMarginRight);
         bformatter.Serialize(stream, richTextBox[teller].printMarginBottom);
         bformatter.Serialize(stream, tabPages[teller].Text.ToString());
         bformatter.Serialize(stream, tabPages[teller].Tag.ToString());
         richTextBox[teller].SelectAll();
         bformatter.Serialize(stream, richTextBox[teller].SelectedRtf);
         richTextBox[teller].DeselectAll();
    }
    stream.Close();
    sbpMenu.Text = "Gereed";
}

To get back the pages from a serialized file, you must work in the same order as the tap-pages are serialized. We start to save the existed tab pages and clear the tab control. After loading the number of tab pages, we add the number of tab pages and clear the old content of the rich-text box. Of course, we load the page settings of the PageSetupDialog and the properties text and the tag, before the count of the rich-text box is loaded.

private void deserializeFileDialog_FileOk(object sender, CancelEventArgs e)
{
    sbpMenu.Text = "Deserialize alle pagina's";
    //Open the file written above and read values from it.
    saveAll();
    tabControl.TabPages.Clear();
    Stream stream = File.Open(deserializeFileDialog.FileName, FileMode.Open);
    BinaryFormatter bformatter = new BinaryFormatter();
    maxTab = (int)bformatter.Deserialize(stream);
    for (int teller = 0; teller < maxTab; teller++)
    {
         AddPage(teller);
         richTextBox[teller].Clear();
         richTextBox[teller].printLandScape = 
                    (bool)bformatter.Deserialize(stream);
         richTextBox[teller].printMarginTop = 
                    (int)bformatter.Deserialize(stream);
         richTextBox[teller].printMarginLeft = 
                    (int)bformatter.Deserialize(stream);
         richTextBox[teller].printMarginRight = 
                    (int)bformatter.Deserialize(stream);
         richTextBox[teller].printMarginBottom = 
                    (int)bformatter.Deserialize(stream);
         tabPages[teller].Text = bformatter.Deserialize(stream).ToString();
         tabPages[teller].Tag = bformatter.Deserialize(stream).ToString();
         richTextBox[teller].SelectedRtf = 
                     bformatter.Deserialize(stream).ToString();
    }
    stream.Close();
    sbpMenu.Text = "Gereed";
}

Emoticons

We can add little pictures, which can be chosen from a palette.

The palette is a Panel with picture boxes. The Panel is created in the Load-event of the form. The pictures-file is made in Emotion_collector. The serialized file is embedded as a resource file. The file will loaded in a MemoryStream. In the Options box, you can choose to load a picture file from disk.

images = new ImageInfo[maxImage];
for (int teller = 0; teller < maxImage; teller++)
     images[teller] = new ImageInfo();

Loading a a serialized file from the resource will be done with a MemoryStream, the same way as loading a serialized file in the tab pages.

// Load Emoticon Images
try
{
    MemoryStream stream = new MemoryStream(Resources._default);
    BinaryFormatter bformatter = new BinaryFormatter();
    imageCounter = (int)bformatter.Deserialize(stream);
    for (int teller = 0; teller < imageCounter; teller++)
    {
        string dummy = bformatter.Deserialize(stream).ToString();
        dummy = bformatter.Deserialize(stream).ToString();
        images[teller].img = (Image)bformatter.Deserialize(stream);
    }
    stream.Close();
}
catch (ArgumentException ex)
{
        MessageBox.Show(ex.Message.ToString());
}
createImagePanel(images, imageCounter);

The image panel will be shown by clicking on the menu item. The palette must first be hidden, because there is an error if you click twice on the menu-item.

private void contextmenuemoticonsToolStripMenuItem_Click(object sender, 
                                                         EventArgs e)
{
    ToolStripMenuItem item = (ToolStripMenuItem)sender;
    EventArgs args = (EventArgs)e;
            
    if (showForm != null)
    {
        // first hide, you can not twice Show a form.
        showForm.Hide();
        showForm.Location = new Point(rectNormal.X + 200, 
                                      rectNormal.Y + 200);
        showForm.Show();
    }
    else
        MessageBox.Show("No Icons selectede");
}

A picture will be added by clicking on a PictureBox. The object sender contains the PictureBox.

void box_MouseClick(object sender, MouseEventArgs e)
{
    PictureBox box = (PictureBox)sender;
    showForm.Hide();

    int index = ZoekTab();
    try
    {
        richTextBox[index].InsertImage(box.Image);
    }
    catch (Exception _e)
    {
        MessageBox.Show("Rtf Image Insert Error\n\n" + 
                        _e.ToString());
    }
}

Text-Fragments

Using the same way, you can insert text-fragments. Standard text fragments are stored in an array of ToolStripMenuItems. I have four date-items made, and these items get added to a context menu. The context menu will be shown by clicking on the menu item:

private void contextMenuToolStripMenuItem_Click(object sender, EventArgs e)
{
    contextMenu_Text.Show(this, new Point(200, 200));
}

The text-fragments are loaded in the Tag-property of the menu-item. The bool-variable standaard is true if the text is plain text. If the the text is in RTF-format, then standaard is false.

DateTime time = DateTime.Now;
toolStripMenuDatumKort.Tag = time.ToShortDateString() + "\n";
toolStripMenuDatumLang.Tag = time.ToLongDateString() + "\n";
toolStripMenuTijdKort.Tag = time.ToShortTimeString() + "\n";
toolStripMenuTijdLang.Tag = time.ToLongTimeString() + "\n";
toolStripMenuDatumKort.ToolTipText = 
                           time.ToShortDateString() + "\n";
toolStripMenuDatumLang.ToolTipText = 
                           time.ToLongDateString() + "\n";
toolStripMenuTijdKort.ToolTipText = 
                           time.ToShortTimeString() + "\n";
toolStripMenuTijdLang.ToolTipText = 
                           time.ToLongTimeString() + "\n";
standaard = true;

When you move the mouse over the context menu items, a preview window will show the items. In the constructor of TabText is set the SplitterDistance equal to the splitter width.

splitContainer.SplitterDistance = splitContainer.Size.Width;

In the MouseHover event, the SplitterDistance will be set on two-thirds of the width and the preview-window will be shown.

private void cmenu_Teksten_MouseHover(object sender, EventArgs e)
{
    ToolStripMenuItem item = (ToolStripMenuItem)sender;
    splitContainer.SplitterDistance = (splitContainer.Size.Width / 3) * 2;
    richTextBoxPreview.Clear();
    if (standaard)
        richTextBoxPreview.AppendText(item.Tag.ToString());
    else
        richTextBoxPreview.AppendRtf(item.Tag.ToString());
}

When the context-menu is closed, the preview-window will disappear with the event Closed of the context menu.

private void cmenu_Teksten_Closed(object sender, 
             ToolStripDropDownClosedEventArgs e)
{
    splitContainer.SplitterDistance = splitContainer.Size.Width;
}

Text-fragments can be made with serialization of the tab-pages. In the Options box, you can choose and load a text-fragment-file from disk.

Help

There is help for the user available in three ways:

  • the usual tooltips.
  • extra information on the status bar.
  • tap-pages with user-information.

Information for the status bar is stored in the Tag property of a menu item. When you move with the mouse over a menu item, a MouseHover event takes place and shows the info on the status bar.

private void toolStripMenuItem_MouseHover(object sender, EventArgs e)
{
    ToolStripMenuItem menu = (ToolStripMenuItem)sender;
    sbpMenu.Text = (string)menu.Tag;
}

If the mouse leaves a menu-item, then the MouseLeave event occurs and resets the text.

private void toolStripMenuItem_MouseLeave(object sender, EventArgs e)
{
    sbpMenu.Text = "Ready";
}

In the Help menu, there is help available for the user. Click on Help in the Help menu and the split container changes for a tab control with tab-pages. The most important thing here is the order of adding of the help control and the menu control.

private void helpHelpToolStripMenuItem_Click(object sender, EventArgs e)
{
    Controls.Remove(this.splitContainer);
    Controls.Remove(this.comboStrip);
    Controls.Remove(this.buttonStrip);
    Controls.Remove(this.menuStrip);
    Controls.Remove(this.statusStrip);
    Controls.Add(helpControl);
    Controls.Add(helpMenu);
}

Clicking on the back-button of the help menu will change the help menu with the split container.

public void backToolStripMenuItem_Click(object sender, EventArgs e)
{
    Controls.Add(splitContainer);
    Controls.Add(comboStrip);
    Controls.Add(buttonStrip);
    Controls.Add(menuStrip);
    Controls.Add(statusStrip);
    Controls.Remove(helpControl);
    Controls.Remove(helpMenu);
}

The help menu control is declared as a variable. You can see that, it is exactly the same as deserialized tabpages, except that there is a memory-stream used.

this.TabPages.Clear();
MemoryStream stream =new MemoryStream(Resources.HelpFile);
BinaryFormatter bformatter = new BinaryFormatter();
int maxTab = (int)bformatter.Deserialize(stream);
for (int teller = 0; teller < maxTab; teller++)
{
    AddPage(teller);
    richTextBox[teller].Clear();
    tabPage[teller].Text = bformatter.Deserialize(stream).ToString();
    tabPage[teller].Tag = bformatter.Deserialize(stream).ToString();
    richTextBox[teller].SelectedRtf = bformatter.Deserialize(stream).ToString();
}
stream.Close();

Changing languages

Because the information is stored in three properties of a ToolStripMenuItem, you can easily switch the language option;

private void languagetoolStripComboBox_SelectedIndexChanged(object sender, 
                                                            EventArgs e)
{
    ToolStripComboBox box = (ToolStripComboBox)sender;
    language = box.Text.ToString();
    switch (box.Text)
    {
        case "Nederlands":
        {
            Nederlands();
            break;
        }
        case "English":
        {
             English();
             break;
        }
    }
}

A part of the function English looks like:

void English()
{
    // Menu Bestand
    fileToolStripMenuItem.Text = "&File";
    // New menu
    newToolStripMenuItem.Tag = "Save the ecxist files, close " + 
                               "all the tab-pages and" + 
                               " make a new tab-page";
    newToolStripMenuItem.Text = "&New";
    newToolStripMenuItem.ToolTipText = "Make a new document";
    ....
    newToolStripButton.Tag = newToolStripMenuItem.Tag.ToString();
    newToolStripButton.ToolTipText = 
                       newToolStripMenuItem.ToolTipText.ToString();
}

Embedded resources

Here, in short, you will see how you can add an embedded resource to your project:

  • Make tab-pages as needed, enter the text, and format it.
  • Serialize the pages under the name HelpFile.srl.
  • Add the file to the project by clicking wit the right mouse button on TabText in the Solution Explorer. Add as an existing item, the file to the project.
  • Set the property Build Action of the file HelpFile.srl to Embedded Resource.
  • Open in the Solution Explorer, Resources.resx.
  • Add resources -> Add Existing File, the file HelpFile.srl.
  • Now can you use Resources.HelpFile to access the file.

Keyboard-state

To get the state of the keys Caps, Insert, ScrollLock and Numlock, we need the class Keyboard. These class is available in the namespace Microsoft.VisualBasic.Devices. To use this namespace, you must the required reference.

  • Go to the Solution Explorer.
  • Right click with the mouse on Reference.
  • Add Reference.
  • Choose in the tab .NET the item "Microsoft.VisualBasic".
  • Then click OK.

So now, you can:

using Microsoft.VisualBasic.Devices;

and declare in the constructor of TabText, the class:

public Keyboard board = new Keyboard();

In the TimerTick event, the status can be read:

private void TabText_TimerTick(object sender, EventArgs e)
{
   DateTime dt = DateTime.Now;
   sbpDate.Text = dt.ToShortDateString();
   sbpTime.Text = dt.ToShortTimeString();

   if (board.CapsLock)
       sbpCapsKey.Text = "Caps on";
   else
       sbpCapsKey.Text = "Caps off";
   if (board.NumLock)
       sbpNumberKey.Text = "Num on";
   else
       sbpNumberKey.Text = "Num off";
   if (board.ScrollLock)
       sbpScrollLock.Text = "Scroll on";
   else
       sbpScrollLock.Text = "Scroll off";

   int index = ZoekTab();
   if (richTextBox[index].GetKeyStateInsert() == 1)
       sbpInsert.Text = "Insert";
   else
       sbpInsert.Text = "Overwrite";
}

sbpDate, sbpTime, sbpCapsKey, sbpNumberKey, sbpScrollLock, and sbpInsert are labels on the status bar.

But the class Keyboard has no property for the Insert-key. So we need an API-function, which I have placed in the RichTextBox.

[DllImportAttribute("user32.dll")]
private static extern uint GetKeyState(int keystate);

public uint GetKeyStateInsert()
{
    return GetKeyState(45); // Keynumber InsertKey
}

Register the program

In the Load function of the form, the settings of the program will be loaded from the registry. In the Form Closed event, the settings will be stored in the registry.

Further information

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Compenkie
Web Developer
Netherlands Netherlands
No Biography provided

Comments and Discussions

 
Bugwhen the "face='宋体'" error Pinmemberzhaohuijun198223-Jun-13 21:32 
GeneralMy vote of 5 PinmemberJeffery.Sun1-Oct-10 20:22 
QuestionGreat article & license Pinmemberlukas252531-May-08 3:23 
GeneralPlease Reply! Pinmembermohadese17-Aug-07 0:27 
QuestionATL [modified] Pinmembershowny26-Jun-07 23:39 
Generalfinding text Pinmemberbalakpn9-Mar-07 1:00 
QuestionAbout license? Pinmemberevraphab8-Mar-07 2:44 
GeneralI don't think the File/New is working Pinmemberdatavalue17-Feb-07 20:13 
General*.bmp , *.jpg PinmemberSixcode29-Dec-06 2:34 
GeneralRe: *.bmp , *.jpg PinmemberSixcode29-Dec-06 3:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 10 Jul 2006
Article Copyright 2006 by Compenkie
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid