Click here to Skip to main content
15,891,423 members
Articles / Mobile Apps

Arabic Support for Windows CE

Rate me:
Please Sign up or sign in to vote.
4.90/5 (57 votes)
19 Jan 2003CPOL3 min read 387.4K   48   155
This article explains how to support the Arabic language in your applications written for Windows CE.

Sample Image - arabicsupport.gif

Introduction

When Microsoft Windows CE first came to life and the number of PDAs based on it increased rapidly, the software market was opened widely for a new type of applications and we all wanted to write new applications or to port our application already written for Win32 to Windows CE. Unfortunately, for the Arabic market, there is no Arabic language support in WinCE 2.11 and WinCE 3.0.

After a long time some third parties introduced some solutions and each claimed “We have the first and best Arabic Language Support for Windows CE (or PocketPC)”. They offered their solutions for a big amount of what is called “money”. Too big for a poor freelance programmer to handle. And after one of the companies offering the solution asked me $5000 for their solution (don’t tell them I told you it was bad), I started to write the first and best Arabic Language support for Windows CE myself. It was about 14 months ago. And here is what I did.

Problem Identification

The main problem is that the WinCE 2.11 and WinCE 3.0 do not have intrinsic support for BiDi (Bi-Directional) fonts and RTL (Right To Left) window controls. I don’t know why. I think it’s not too difficult and I also think that, it will not take much memory nor processing time to support those features.

Before I go deep with you, I wanted you to notice what an Arabic support solution makes to a string to be displayed the right way on a window.

The Arabic string is stored from left to right as in English. But when it’s to be displayed, the first character in the string should be the right most character. Further, in Arabic, the character can take more than one glyph (figure or shape) depending on its position in the word: at first, in middle, at last, and alone. In Arabic Enabled Windows and Unicode ready Windows (Windows 2000 and up) all this is implemented intrinsically in the GDI. In WinCE we have to do this ourselves.

The solution

First, You have to install an Arabic TrueType font. The Tahoma of Arabic-Enabled Windows is great.

Arabization involves reversing the string, considering that numbers shouldn’t be reversed and perform glyph substitution manually.

To reverse the string I use:

//////////////////////////////////////////////////////////////////////
void ArabicReverse(CString &s)
{
    CString out, rev;
    s.MakeReverse();

    int i=0;
    while(i<s.GetLength())
    {
        if((s[i]>='0' && s[i]<='9'))    // isDigit(s[i]) ?
        {
            rev.Empty();
            while((s[i]>='0' && s[i]<='9'))    // isDigit(s[i]) ?
            {
                rev = rev + s[i];
                ++i;
            }
            rev.MakeReverse();
            out = out + rev;
        }
        else
        {
            out = out + s[i];
            ++i;
        }
    }
    s=out;
}
//////////////////////////////////////////////////////////////////////

The second step is a bit more tricky so be ready. We know that the Arabic string in the memory contains the Unicode characters that take the same form of the isolated glyphs of the letters. We have to make mapping from the character to the right glyphs to be displayed, considering all states of the letter.

I some how could construct an array of all Arabic letters' state. It is not sorted alphabetically. The first row in the table is the first piece of data I could collect about a letter and it was the letter ‘thal’.

As far as I know .. the rules of glyph substitution are somewhere in the TTF file but I found it very complex. To help constructing the rules for glyph substitution, I made two sets of characters according to whether it may need a link to the previous or the next character.

//////////////////////////////////////////////////////////////////////
CString Arabize (LPCTSTR in)
{
    static struct
    {
        WCHAR character;
        WCHAR endGlyph;
        WCHAR iniGlyph;
        WCHAR midGlyph;
        WCHAR isoGlyph;
    }a[N_DISTINCT_CHARACTERS]=
    {
        {0x630, 0xfeac, 0xfeab, 0xfeac, 0xfeab},
        {0x62f, 0xfeaa, 0xfea9, 0xfeaa, 0xfea9},
        {0x62c, 0xfe9e, 0xfe9f, 0xfea0, 0xfe9d},
        {0x62d, 0xfea2, 0xfea3, 0xfea4, 0xfea1},
        {0x62e, 0xfea6, 0xfea7, 0xfea8, 0xfea5},
        {0x647, 0xfeea, 0xfeeb, 0xfeec, 0xfee9},
        {0x639, 0xfeca, 0xfecb, 0xfecc, 0xfec9},
        {0x63a, 0xfece, 0xfecf, 0xfed0, 0xfecd},
        {0x641, 0xfed2, 0xfed3, 0xfed4, 0xfed1},
        {0x642, 0xfed6, 0xfed7, 0xfed8, 0xfed5},
        {0x62b, 0xfe9a, 0xfe9b, 0xfe9c, 0xfe99},
        {0x635, 0xfeba, 0xfebb, 0xfebc, 0xfeb9},
        {0x636, 0xfebe, 0xfebf, 0xfec0, 0xfebd},
        {0x637, 0xfec2, 0xfec3, 0xfec4, 0xfec1},
        {0x643, 0xfeda, 0xfedb, 0xfedc, 0xfed9},
        {0x645, 0xfee2, 0xfee3, 0xfee4, 0xfee1},
        {0x646, 0xfee6, 0xfee7, 0xfee8, 0xfee5},
        {0x62a, 0xfe96, 0xfe97, 0xfe98, 0xfe95},
        {0x627, 0xfe8e, 0xfe8d, 0xfe8e, 0xfe8d},
        {0x644, 0xfede, 0xfedf, 0xfee0, 0xfedd},
        {0x628, 0xfe90, 0xfe91, 0xfe92, 0xfe8f},
        {0x64a, 0xfef2, 0xfef3, 0xfef4, 0xfef1},
        {0x633, 0xfeb2, 0xfeb3, 0xfeb4, 0xfeb1},
        {0x634, 0xfeb6, 0xfeb7, 0xfeb8, 0xfeb5},
        {0x638, 0xfec6, 0xfec7, 0xfec8, 0xfec5},
        {0x632, 0xfeb0, 0xfeaf, 0xfeb0, 0xfeaf},
        {0x648, 0xfeee, 0xfeed, 0xfeee, 0xfeed},
        {0x629, 0xfe94, 0xfe93, 0xfe93, 0xfe93},
        {0x649, 0xfef0, 0xfeef, 0xfef0, 0xfeef},
        {0x631, 0xfeae, 0xfead, 0xfeae, 0xfead},
        {0x624, 0xfe86, 0xfe85, 0xfe86, 0xfe85},
        {0x621, 0xfe80, 0xfe80, 0xfe80, 0xfe7f},
        {0x626, 0xfe8a, 0xfe8b, 0xfe8c, 0xfe89},
        {0x623, 0xfe84, 0xfe83, 0xfe84, 0xfe83},
        {0x622, 0xfe82, 0xfe81, 0xfe82, 0xfe81},
        {0x625, 0xfe88, 0xfe87, 0xfe88, 0xfe87}
    };
    BOOL linkBefore, linkAfter;
    CString out;
    out=in;
    for(UINT i=0; i<_tcslen(in); i++)
    {
        WCHAR ch=in[i];
        if(((ch>=0x0621 && ch<=0x064a)) // is an Arabic character?
        {
            int idx = 0;
            while (idx < N_DISTINCT_CHARACTERS)
            {
                if (a[idx].character == in[i])
                    break;
                ++idx;
            }
            
            if(i == _tcslen(in) - 1)
                linkAfter=0;
            else
                linkAfter = (isFromTheSet1(in[i+1]) || 
                                   isFromTheSet2(in[i+1]));
            if(i == 0)
                linkBefore=0;
            else
                linkBefore=isFromTheSet1(in[i-1]);
    
            if(linkBefore && linkAfter)
                out.SetAt(i, a[idx].midGlyph);
            if(linkBefore && !linkAfter)
                out.SetAt(i, a[idx].endGlyph);
            if(!linkBefore && linkAfter)
                out.SetAt(i, a[idx].iniGlyph);
            if(!linkBefore && !linkAfter)
                out.SetAt(i, a[idx].glyph);
        }
    }
    ArabicReverse (out);
    return out;
}
//////////////////////////////////////////////////////////////////////
BOOL BzArabicRender::isFromTheSet1(WCHAR ch)
{
    static WCHAR theSet1[22]={
        0x62c, 0x62d, 0x62e, 0x647, 0x639, 0x63a, 0x641, 0x642,
        0x62b, 0x635, 0x636, 0x637, 0x643, 0x645, 0x646, 0x62a,
        0x644, 0x628, 0x64a, 0x633, 0x634, 0x638};
    int i = 0;
    while (i < 22)
    {
        if(ch == theSet1[i])
            return TRUE;
        ++i;
    }
    return FALSE;
}
//////////////////////////////////////////////////////////////////////
BOOL BzArabicRender::isFromTheSet2(WCHAR ch)
{
    static WCHAR theSet2[12]={
        0x627, 0x623, 0x625, 0x622, 0x62f, 0x630, 0x631, 0x632,
        0x648, 0x624, 0x629, 0x649};
    int i = 0;
    while (i < 12)
    {
        if(ch == theSet2[i])
            return TRUE;
        ++i;
    }
    return FALSE;
}
//////////////////////////////////////////////////////////////////////

To use this .. just SetWindowText the window you want to the output of the Arabize().

window.SetWindowText(str); //before
window.SetWindowText(Arabize(str)); //after

or

dc.DrawText(str,……); //before
dc.DrawText(Arabize(str),……); //after

… etc

So there is not much change in code if you already have written old code for English.

Of course it’s not the ideal solution. I think the optimum solution is to hook the DrawText function and add all this stuff. But when I wrote this code there was no online material showing how to hook Windows CE APIs.

I will update the document to show how to retrieve the characters from the glyphs to process them soon. Or it may be another article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer www.mmonem.com
Egypt Egypt
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralA sample VB Or C# project Pin
osamasafwat29-Aug-04 2:43
osamasafwat29-Aug-04 2:43 
GeneralRe: A sample VB Or C# project Pin
AhmedEssamNaiem5-Oct-07 23:18
AhmedEssamNaiem5-Oct-07 23:18 
GeneralPersian words ! Pin
Hadi Rezaee18-Jun-04 23:22
Hadi Rezaee18-Jun-04 23:22 
GeneralRe: Persian words ! Pin
Hadi Rezaee26-Jun-04 4:45
Hadi Rezaee26-Jun-04 4:45 
QuestionCan I get your E-mail Pin
Samy Abd El-Rahman30-May-04 1:17
Samy Abd El-Rahman30-May-04 1:17 
GeneralCommercial Level Pin
Member 1873302-May-04 18:42
Member 1873302-May-04 18:42 
GeneralRe: Commercial Level Pin
Anonymous27-Oct-04 2:06
Anonymous27-Oct-04 2:06 
GeneralVery Nice Work. Here is a c# contribution Pin
Member 50679417-Apr-04 22:52
Member 50679417-Apr-04 22:52 
Hello

I would like to thank you very much for this article. Which has been very helpful to me. Since my applicatin is in C# I had to migrate your code. I also made some optimizations, handling lam alef, etc. Please find my code below.

using System;
using System.Data;
using System.Text;


namespace GNS.Mobility.Utilities
{
	public sealed class Arabization
	{

		static Arabization()
		{
			arabicArray = new int[lookupArray.Length];
			for(int i = 0; i < lookupArray.GetLength(0); i++)
			{
				for(int j = 0; j < lookupArray.GetLength(1); j++)
				{
					arabicArray[i * lookupArray.GetLength(1) + j] = lookupArray[i, j];
				}
			}
			QuickSort(arabicArray, 0, arabicArray.Length -1);
			arabicSearchStart = 0;
			while(arabicArray[arabicSearchStart] == 0)
				arabicSearchStart++;
		}

		private Arabization()
		{
			
		}

		private static void QuickSort(int[] array, int left, int right)
		{
			do 
			{
				int i = left;
				int j = right;
				// pivot
				int x = array[(i + j) >> 1];

				do 
				{
					while(array[i] < x) i++;
					while(x < array[j]) j--;
					
					if (i > j) break;
					if (i < j) 
					{
						int val = array[i];
						array[i] = array[j];
						array[j] = val;
					}
					i++;
					j--;
				}
				while (i <= j);
				// sort the elements
				if (j - left <= right - i) 
				{
					if (left < j) QuickSort(array, left, j);
					left = i;
				}
				else 
				{
					if (i < right) QuickSort(array, i, right);
					right = j;
				}
			}
			while (left < right);
		}

		private static int BinarySearch(int[] array, int val)
		{
			int u, l, m, a;
			l = array == arabicArray ? arabicSearchStart : 0;
			u = array.Length - 1;
			while(l <= u)
			{
				m = (l + u) >> 1;
				a = array[m];
				if(a == val)
				{
					return m;
				}
				else if(a < val)
				{
					l = m + 1;
				}
				else
					u = m - 1;
			}
			return ~l;
		}

		private const int endIdx = 1;
		private const int iniIdx = 2;
		private const int midIdx = 3;
		private const int isoIdx = 4;
		private static readonly int[] arabicArray;
		private static readonly int arabicSearchStart;

		private static readonly int[,] lamalefarray = 
		{
			{0x622, 0xfef6, 0xfef5, 0xfef6, 0xfef5},	// alef mad
			{0x623, 0xfef8, 0xfef7, 0xfef8, 0xfef7},	// alef hamza
			{0x625, 0xfefa, 0xfef9, 0xfefa, 0xfef9},	// alef hamza below
			{0x627, 0xfefc, 0xfefb, 0xfefc, 0xfefb},	// alef
			
		};

		private static readonly int[,] lookupArray =
		{
			// uni, end	  , init  , mid	  , iso	
			{0x621, 0xfe80, 0xfe80, 0xfe80, 0xfe80},	// hamza
			{0x622, 0xfe82, 0xfe81, 0xfe82, 0xfe81},	// alef mad
			{0x623, 0xfe84, 0xfe83, 0xfe84, 0xfe83},	// alef hamza
			{0x624, 0xfe86, 0xfe85, 0xfe86, 0xfe85},	// waw hamza
			{0x625, 0xfe88, 0xfe87, 0xfe88, 0xfe87},	// alef hamza below
			{0x626, 0xfe8a, 0xfe8b, 0xfe8c, 0xfe89},	// yeh with hamza
			{0x627, 0xfe8e, 0xfe8d, 0xfe8e, 0xfe8d},	// alef
			{0x628, 0xfe90, 0xfe91, 0xfe92, 0xfe8f},	// beh
			{0x629, 0xfe94, 0xfe93, 0xfe93, 0xfe93},	// teh marboota
			{0x62a, 0xfe96, 0xfe97, 0xfe98, 0xfe95},	// teh
			{0x62b, 0xfe9a, 0xfe9b, 0xfe9c, 0xfe99},	// theh
			{0x62c, 0xfe9e, 0xfe9f, 0xfea0, 0xfe9d},	// jeem
			{0x62d, 0xfea2, 0xfea3, 0xfea4, 0xfea1},	// hah
			{0x62e, 0xfea6, 0xfea7, 0xfea8, 0xfea5},	// khah
			{0x62f, 0xfeaa, 0xfea9, 0xfeaa, 0xfea9},	// dal
			{0x630, 0xfeac, 0xfeab, 0xfeac, 0xfeab},	// dhal
			{0x631, 0xfeae, 0xfead, 0xfeae, 0xfead},	// reh
			{0x632, 0xfeb0, 0xfeaf, 0xfeb0, 0xfeaf},	// zen
			{0x633, 0xfeb2, 0xfeb3, 0xfeb4, 0xfeb1},	// seen
			{0x634, 0xfeb6, 0xfeb7, 0xfeb8, 0xfeb5},	// sheen
			{0x635, 0xfeba, 0xfebb, 0xfebc, 0xfeb9},	// sad
			{0x636, 0xfebe, 0xfebf, 0xfec0, 0xfebd},	// dad
			{0x637, 0xfec2, 0xfec3, 0xfec4, 0xfec1},	// tah
			{0x638, 0xfec6, 0xfec7, 0xfec8, 0xfec5},	// zah
			{0x639, 0xfeca, 0xfecb, 0xfecc, 0xfec9},	// ain
			{0x63a, 0xfece, 0xfecf, 0xfed0, 0xfecd},	// ghain
			{0x63b, 0, 0, 0, 0},						// Not Used
			{0x63c, 0, 0, 0, 0},						// Not Used
			{0x63d, 0, 0, 0, 0},						// Not Used
			{0x63e, 0, 0, 0, 0},						// Not Used
			{0x63f, 0, 0, 0, 0},						// Not Used
			{0x640,	0x0640, 0x0640, 0x0640, 0x0640},	// tatweel  (Special case no conversion)
			{0x641, 0xfed2, 0xfed3, 0xfed4, 0xfed1},	// feh
			{0x642, 0xfed6, 0xfed7, 0xfed8, 0xfed5},	// qaf
			{0x643, 0xfeda, 0xfedb, 0xfedc, 0xfed9},	// kaf
			{0x644, 0xfede, 0xfedf, 0xfee0, 0xfedd},	// lam
			{0x645, 0xfee2, 0xfee3, 0xfee4, 0xfee1},	// meem
			{0x646, 0xfee6, 0xfee7, 0xfee8, 0xfee5},	// noon
			{0x647, 0xfeea, 0xfeeb, 0xfeec, 0xfee9},	// heh
			{0x648, 0xfeee, 0xfeed, 0xfeee, 0xfeed},	// waw
			{0x649, 0xfef0, 0xfeef, 0xfef0, 0xfeef},	// alef layena
			{0x64a, 0xfef2, 0xfef3, 0xfef4, 0xfef1},	// yeh
			{0x64b, 0, 0, 0, 0},						// fatheten
			{0x64c, 0, 0, 0, 0},						// dammeten
			{0x64d, 0, 0, 0, 0},						// kasreten
			{0x64e, 0, 0, 0, 0},						// fatha
			{0x64f, 0, 0, 0, 0},						// damma
			{0x650, 0, 0, 0, 0},						// kasra
			{0x651, 0, 0, 0, 0},						// shadda
			{0x652, 0, 0, 0, 0},						// sukun
		};

		private const int lamUnicode = 0x644;

		private static readonly int[] set1 = 
		{
			0x626, 0x628, 0x62a, 0x62b, 0x62c, 0x62d, 0x62e, 0x633, 
			0x634, 0x635, 0x636, 0x637, 0x638, 0x639, 0x63a, 0x640, 
			0x641, 0x642, 0x643, 0x644, 0x645, 0x646, 0x647, 0x64a
		};

		private static readonly int[] set2 = 
		{
			0x622, 0x623, 0x624, 0x625, 0x627, 0x629, 0x62f, 0x630, 
			0x631, 0x632, 0x648, 0x649
		};

		private static bool IsArabic(char ch)
		{
			int ch1 = ch;
			if((((ch1 & 0xff00) ^ 0x0600) != 0) && (((ch1 & 0xff00) ^ 0xfe00) != 0))
				return false;
			return BinarySearch(arabicArray, ch1) >= 0;
		}

		private static bool IsGenericArabic(char ch)
		{
			int ch1 = (int)ch;
			return ch1 >= 0x0621 && ch1 <= 0x064a;
		}

		private static bool IsInSet1(char ch)
		{
			return BinarySearch(set1, (int)ch) >= 0;
		}

		private static bool IsInSet2(char ch)
		{
			return BinarySearch(set2, (int)ch) >= 0;
		}

		private static void StringReverse(StringBuilder input, int index, int length)
		{
			for(int i = 0; i < length; i++)
			{
				int revind = length - i - 1;
				if(revind <= i)
					return;
				char temp = input[index + revind];
				input[index + revind] = input[i + index];
				input[i + index] = temp;
			}
		}

		private static void StringReverse(StringBuilder input)
		{
			StringReverse(input, 0, input.Length);
		}

		private static void ArabicReverse(StringBuilder input)
		{
			StringReverse(input);

			for(int i = 0; i < input.Length; i++)
			{
				if(!IsArabic(input[i]))
				{
					int len = 0;
					while(((i + len) < input.Length) && !IsArabic(input[i + len]))
					{
						len++;
					}
					StringReverse(input, i, len);
					i += len - 1;
				}
			}
		}

		public static string Arabize(string input)
		{
			bool linkBefore, linkAfter;
			// using a string builder to avoid many string allocations
			StringBuilder sbout = new StringBuilder(input);
			for(int i = 0, k = 0; i < input.Length; i++, k++)
			{
				char ch = input[i];
				int idx = (int)ch - lookupArray[0, 0];
				if(idx >= 0 && idx < lookupArray.GetLength(0))
				{
					if(i == (input.Length - 1))
						linkAfter = false;
					else
						linkAfter = IsInSet1(input[i + 1]) || IsInSet2(input[i + 1]);
					if(i == 0)
						linkBefore = false;
					else
						linkBefore = IsInSet1(input[i - 1]);

					// Handling lam alef combinations
					if(linkAfter && (int)ch == lamUnicode)
					{
						char ch1 = input[i + 1];
						int j;
						for(j = 0; j < lamalefarray.GetLength(0); j++)
						{
							if((int)ch1 == lamalefarray[j, 0])
							{
								sbout[k] = (char)lamalefarray[j, linkBefore ? midIdx : iniIdx];
								sbout.Remove(k + 1, 1);
								i++;
								break;
							}
						}
						if(j < lamalefarray.GetLength(0))
							continue;
					}
					
					if(linkAfter && linkBefore)
						sbout[k] = (char)lookupArray[idx, midIdx];
					else if(linkBefore && !linkAfter)
		                sbout[k] = (char)lookupArray[idx, endIdx];
					else if(!linkBefore && linkAfter)
						sbout[k] = (char)lookupArray[idx, iniIdx];
					else if(!linkBefore && !linkAfter)
						sbout[k] = (char)lookupArray[idx, isoIdx];

					// Removing tashkeel characters
					if((int)sbout[k] == 0)
					{
						sbout.Remove(k, 1);
					}
				}
				else if(IsArabic(ch))
					return input;
			}
			ArabicReverse(sbout);
			return sbout.ToString();
		}
	}
}

GeneralRe: Very Nice Work. Here is a c# contribution Pin
ttalal29-Sep-05 2:46
ttalal29-Sep-05 2:46 
GeneralRe: Very Nice Work. Here is a c# contribution Pin
mariane.sobhy8-Aug-07 1:57
mariane.sobhy8-Aug-07 1:57 
GeneralRe: Very Nice Work. Here is a c# contribution Pin
stankovski11-Sep-09 11:40
stankovski11-Sep-09 11:40 
GeneralRe: Very Nice Work. Here is a c# contribution Pin
ttalal9-Oct-05 22:55
ttalal9-Oct-05 22:55 
GeneralRe: Very Nice Work. Here is a c# contribution Pin
Microsoftawy20-Feb-06 23:56
Microsoftawy20-Feb-06 23:56 
GeneralRe: Very Nice Work. Here is a c# contribution Pin
basicci@hotmail.com23-Jun-08 22:01
basicci@hotmail.com23-Jun-08 22:01 
GeneralHi,It's really perfect! I have some truble with Testing this Class.Shall you give me a hand... Pin
Mead Lai5-May-09 1:25
Mead Lai5-May-09 1:25 
GeneralNice work Pin
Fad B2-Apr-04 2:09
Fad B2-Apr-04 2:09 
GeneralRe: Nice work Pin
Anonymous11-May-04 10:36
Anonymous11-May-04 10:36 
GeneralNeed help in Arabic application Pin
mr_aladddin21-Mar-04 10:12
mr_aladddin21-Mar-04 10:12 
GeneralRe: Need help in Arabic application Pin
project_manager12-Jul-07 1:58
project_manager12-Jul-07 1:58 
Generalhelp !! Pin
kuay23-Feb-04 4:51
kuay23-Feb-04 4:51 
QuestionARABIC_CHARSET? Pin
AAAspacedog75-Feb-04 8:16
sussAAAspacedog75-Feb-04 8:16 
AnswerRe: ARABIC_CHARSET? Pin
Mohamed Abdel-Monem6-Feb-04 0:46
Mohamed Abdel-Monem6-Feb-04 0:46 
AnswerRe: ARABIC_CHARSET? Pin
Fad B13-Apr-04 6:37
Fad B13-Apr-04 6:37 
General.NET Code in .NET CF / EVB Pin
Kayomarz4-Feb-04 3:50
Kayomarz4-Feb-04 3:50 
GeneralRe: .NET Code in .NET CF / EVB Pin
Mohamed Abdel-Monem6-Feb-04 0:33
Mohamed Abdel-Monem6-Feb-04 0:33 

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

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