Click here to Skip to main content
11,437,265 members (63,842 online)
Click here to Skip to main content

Steganography - Hiding data in plain sight

, 7 Apr 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
An introduction to Steganography using bitmap files.

Sample Image


This article aims to demonstrate the basic concept of Steganography. It is the practice of placing hidden information within a message. There are a myriad number of ways to hide data within a file. Many file types support header fields that provide space for self describing data. Some well known, some obscure. These can be leveraged to transfer additional/hidden content. Other techniques involve a subtle altering of the data in the file such that on the surface the new file appears identical to the original. In all cases, access to the original file will indicate it has been modified. Decoding of the hidden content in such cases is a function of the encryption algorithm used (if any) to embed the concealed content in the cover file. In this article and its accompanying sample application, I'll be using a basic bitmap file and modifying the least significant bits of the pixel bytes to store the message. The task here is to introduce the concept and detail one possible method as an example. Once an understanding of the basic concept is in place, the type of encryption applied, the subtlety with which the cover file is modified, and the type of cover file used can all be modified to taste.

Structure of a bitmap file

First, a little background material on the type of cover file we'll be using. As an overview, a bitmap is essentially a mapping of the values used to represent the pixels within a given area on a screen. One of the older image formats in use today it consists of three to four parts or sections. Header, Information Header, Palette (optional), and Image Data. The header contains a "magic" identifier (letting us know the file is a bitmap) as well as the file size and the offset in the file where the actual pixel mapping begins. The information section contains such things as width and height of the image, bits per pixel, number of colors, and what (if any) compression type has been used. If "indexed color" has been used, a section containing a table of colors will then be next. Bitmaps using indexed colors do not store the color specification within each pixel but rather an index into the optional section that contains the colors table. We will not be considering indexed color bitmaps within the bounds of our simple Steganography application. The final section contains the actual image data i.e, the map of pixels that make up our bitmap image.

Sample Image

Thankfully, in our simple Steganography application, we only need to focus on the image data section and will ignore the other sections. Our entry point into the bitmap file will be the .Net "Bitmap" class and we'll be using it's GetPixel and SetPixel methods to store and retrieve our hidden data. GetPixel and SetPixel return and are passed a Color object, respectively. By "borrowing bits" from the Color object we will interleave the data we wish to transfer.

Application overview

The sample application this article is based on is a commandline utility that allows for the encoding and decoding of a file into a bitmap in such a manner as to conceal the existence of the embedded file from the casual observer. At a high level the objects that will be doing the work for the application are as follows. SteganographyEngine - this class acts as a wrapper for the Steganography functionality provided. SteganographyEngine manages the file IO for the the various Steganography objects in the application as well as instantiation and invocation of those objects based on parameters passed to it from the application object. SteganographyEngine has a single member variable of type ISteganographyFactory. ISteganographyFactory in turn works with two other Steganography specific objects. IMetaData and IBitmapCodec. Together these three form the basis for all Steganography operations performed by the application. All three are interfaces in order to provide a measure of extensibility to the application. The concrete classes SteganographyFactory, MetaData, and BitmapCodec, respectively provide implementations for each of these interfaces within the application. BitmapCodec is where the rubber hits the road so to speak. This is where the individual bytes are encoded and decoded into the cover files bytes. MetaData is a section of data we encode/decode along with the content we wish to embed. As the name implies it is data about the data we've embedded. This is done to facilitate extraction. SteganographyFactory ties the codec and meta-data classes together performing the needed function calls in order to provide a cohesive Steganography interface to the engine class.

Encoding the data

Each pixel returned by the GetPixel method of the Bitmap class is represented by a Color object. Color objects contain Red, Green, Blue properties and can be represented by one byte each. It is within the last two to three bits of each of these bytes that we will store our data. Thus, in this example each pixel color will provide for us three bytes with which we can encode our data. Each byte ranges from 0 to 255. The higher the number the greater the amount of that color is applied to it's respective pixel. In general, these last three bytes can be modified without having an overly large impact on the bitmap picture. Certainly not enough to notice if one is not familiar with the original. So, each byte to embed will be split between each of the three color bytes. As stated in the application overview the code to encode/decode a byte resides in the class ByteCodec. Below I have detailed the code to encode i.e., splitting and allocating a byte between the red, green, and blue bytes of a Color object.

First the function definition. An array of byte values is passed in. These are the red, green, and blue values of our target pixel's color. Additionally, a single byte value is passed in. This is the value we wish to encode.

void EncodeByte(byte[] ByteArray, byte bytValue)

So "bytValue" is the value we wish to encode within the red, green, and blue color bytes. We have a total of eight bits to encode over a combination of three bytes. Bits are encoded into their target bytes starting at the least significant bit of each byte. As a concrete example we will encode a value of 105 into a pixel with the colors red=34, green=73, and blue=80. The program by default allocates the bits of the byte to encode as the 3 most significant bits to red, next 3 bits to green, and the final 2 bits to blue. We will refer to this as a mask of 3:3:2. Our bit patterns as EncodeByte begins to execute looks as follows.

0110 1001  The value to encode (105)
0010 0010  The red byte of the pixel (34)
0100 1001  The green byte of the pixel (73)
0101 0000  The blue byte of the pixel (80)

From the bits of the value to encode (bytValue) we must transfer bits into our pixel bytes as follows

011  To the red byte
010  To the green byte
01    To the blue byte

As stated, all bit transfers are done starting from the least significant bit of each targeted color byte

For the red byte it looks like this

0010 0010   Red Byte
0000 0011   Bits to encode
0010 0011   New red (35).

The three least significant bits have been replaced

For the green byte we have:

0100 1001  Green Byte
0000 0010  Bits to encode
0100 1010  New green (74).

The three least significant bits have been replaced

And finally for the blue byte:

0101 0000  Blue Byte
0000 0001  Bits to encode
0101 0001  New blue (81).

The two least significant bits have been replaced.

So applying a bit dispersal mask of 3:3:2 to our byte to encode modifies the original color of the pixel just slightly. From a pixel color of red=34, green=73, and blue=80 to a color of red=35, green=74, and blue=81 We have encoded the byte and the picture to the causal observer giving no indication that it contains a second level of information.

As far as the source code for this function, we have what follows below. First, we create bit array objects that corresponds to our byte to encode (bytValue) and the bit dispersal mask we have chosen to use, 3:3:2. This is the default value for the mask. Other values to use for the mask can be passed in from the commandline at run time. The member variables of the ByteCodec class m_bytRedMask, m_bytGreenMask, and m_bytBlueMask correspond to the three values in the bit dispersal mask.

BitArray Source = new BitArray(new byte[] { bytValue });
BitArray SourceA = new BitArray(new byte[] { m_bytRedMask });
BitArray SourceB = new BitArray(new byte[] { m_bytGreenMask });
BitArray SourceC = new BitArray(new byte[] { m_bytBlueMask });

Next we need to separate the bits of the value to encode into their respective red, green, and blue components based on the bit dispersal masks. We use a logical "And" operation on our bit arrays to do this.


At this point "red" bits to encode reside in the bit array SourceA. Similarly "green" bits are in SourceB and "blue" bits are in SourceC.

The bit patterns look like this.

0110 0000  SourceA
0000 1000  SourceB
0000 0001  SourceC

If you look diagonally from the top left to the bottom right of the three patterns you can still see the original bit pattern of the byte to encode 0110 1001

The next step is to shift these bits so that the mask we are using will be applied to the least significant bits of each color byte. Our mask specifies 3:3:2 so in SourceA we are using the first three bits. Those bits must now be shifted into the three least significant bit positions of the red byte. To do this, we employ the shift operator. The code to shift the "red bits" into the red byte is below.

byte[] TempByteArray = new byte[1];
SourceA.CopyTo(TempByteArray, 0);
TempByteArray[0] = (byte)(TempByteArray[0] >> m_bytRedShift);
SourceA = new BitArray(new byte[] { TempByteArray[0] });

The member variable "m_bytRedShift" of the ByteCodec class is the number of bits to shift for red bits. This value is calculated when the class is instantiated based on the bit dispersal mask supplied. In our case 3:3:2. So we are taking the first three bits of our value to encode and that means that we will need to shift them five places to get them into the position of the corresponding least significant bits. The value of "m_bytRedShift" is therefore five. After shifting our bits in the bit array "SourceA" five places we will have the bit pattern 0000 0011. A similar operation must also be done for the "green bits" (below)

TempByteArray = new byte[1];
SourceB.CopyTo(TempByteArray, 0);
TempByteArray[0] = (byte)(TempByteArray[0] >> m_bytGreenShift);
SourceB = new BitArray(new byte[] { TempByteArray[0] });

The green bits however are in the middle of the byte to encode. As such they don't have to travel as far to reach the position of their corresponding least significant bits. Again our mask is 3:3:2 but since we are already 3 bits into the byte from our activities related to the "red" bits, we need to adjust our shift value accordingly. Instead of shifting five bits we shift three bits less. Our green bits are thus shifted only two bits. The value of the member variable "m_bytGreenShift" is therefore two. The bit pattern of SourceB then becomes 0000 0010

For the final portion of our bit dispersal mask we catch a break. "Blue" bits are at the end of the mask and as such don't require any shifting. Go blue bits!

With all our bits in our byte to encode properly positioned it's time to apply them to their corresponding color bytes. These new values will then become part of the bitmap. To merge our bits to encode we need to first clear out the existing bits in each of their corresponding color bytes. This as it turns out is pretty simple. To zero out any bit in a byte is a two step operation. First a logical or is done with the selected bit and a "1" bit (aka "true" or "on"). The result of a "1" and anything will result in a "1". Thus, we have turned on the target bit. Next we want to turn it off to "clear" it for reciept of the bits to encode. We can turn a bit with a value of "1" off by applying a logical "XOR" operation against a value of "1". An XOR with two bits having a value of "1" will yield a "0" and the bit is turned off. A concrete example will hopefully make this more clear. Remember that our pixel has a red byte with a value of 34 as it enters the function for a bit pattern of 0010 0010. It still has this value. We wish to zero out the three least significant bits of this byte. First we place the red byte (value of 34) that was passed into our function into a bit array object as below.

BitArray TargetRed = new BitArray(new byte[] { ByteArray[0] });

TargetRed now has a bit pattern of 0010 0010. The "red" portion of our bit dispersal mask, held in the member variable m_bytRedClearByte, has a bit pattern that looks like this 0000 0111. We place that value into a bit array as well and or the two like this.

TargetRed.Or(new BitArray(new byte[] { m_bytRedClearByte }));

From a bit perspective it looks something like this

0010 0010
0000 0111
0010 0111

Our three least significant bits have now been turned on. We immediately follow up this operation by again applying the bit pattern of m_bytRedClearByte but this time we use an "XOR" operation. The code looks like this.

TargetRed.Xor(new BitArray(new byte[] { m_bytRedClearByte }));

And from a bit perspective we have

0010 0111
0000 0111
0010 0000

We have cleared out the bits that correspond to our red mask in our red byte.

Finally, we move the bits to encode into our red byte with a logical "OR" operation.


This operation from the a bit view looks like this

0010 0000  Current value of red byte
0000 0011
0010 0011

We now have our new value for the red byte of our pixel (35). We do this for the green and blue bits as well to encode them into their corresponding color bytes. This process is followed for all the bytes to encode and a new bitmap is then produced which is subtly modified to hold our hidden data. Before we move on to decoding the data however, we have to address one important issue. Once we encode our data we lose some critical information regarding it. Most importantly how much data has been encoded. Without knowing this vital piece of information we won't know when to stop decoding bytes and will simply append unrelated data onto our embedded data when decoding. What we need to do is encode some data about our data.

Data about our data

It's not only helpful but actually essential to place some meta data in our bitmap before shifting in the embedded file into the bitmap's pixel section. As stated, without at least knowing the amount of data that has been embedded we will not know when to stop decoding and will append unrelated bytes into our decoded data. In our sample application we allocate a meta-data area of 50 bytes that immediately precedes the actual data we will embed. In the meta-data are contained the following pieces of information. First the length of meta-data section. This is always the first byte we embed. Since it occupies only a single byte, our meta-data is limited to a maximum of 255 bytes. The next meta-data item stored is the length of the file we have embedded. This is a long value and occupies the next eight bytes in our meta-data. We also like to store the name of the file embedded. Useful information to have. Before we can do this however we need to store the length of that file name string. This is an integer value and as such will occupy the next four bytes. Once the length of the string has been added we then encode each character of the file name. Finally the last value to be encoded as part of our meta-data is a checksum for the meta-data. The check sum for our sample application meta-data is the sum of the length of the file, the length of the encoded file name, and the numeric value of the last character in the file name. The checksum is a long value and occupies eight bytes. Any bytes not used in the meta-data section are left un-encoded. To summarize, the meta-data section looks as follows:

1 byte Length of Meta-data
8 bytes Length of encoded file
4 bytes Length of file name
n bytes File name
8 bytes checksum

Decoding the data

Now that we've encoded our data as well as the meta-data that will allow for it's extraction, it's time to go over the decoding process. Like encoding, decoding is done in the BitmapCodec class. The function is DecodeByte and its signature is below.

byte DecodeByte(byte[] ByteArray)

DecodeByte is passed a byte array containing a grouping of pixel bytes that together, when decoded, will yield a single decoded byte. While it can be said that decoding is simply the reverse of encoding, the devil is always in the details of such things. Here I will lay out in detail how we accomplish the decoding of a byte. Assuming that we have been passed a sufficient quantity of bytes that will yield a decoded byte our first step is to extract the encoded bits from the red, green, and blue pixel bytes. To do this we need to place the contents of the red, green, and blue bytes into bit arrays so that we can easily manipulate the individual bits. The code is below.

BitArray SourceA = new BitArray(new byte[] { ByteArray[0] });
BitArray SourceB = new BitArray(new byte[] { ByteArray[1] });
BitArray SourceC = new BitArray(new byte[] { ByteArray[2] });

We'll start decoding with the red bits and the green and blue will follow a similar manner. We are still following the default bit dispersal mask for the sample application of 3:3:2. You must always use the same mask to decode as you used to encode. That said we are looking to extract three encoded bits from our red byte. These bits will be the least significant bits in the red byte. To extract them the first step will be to zero out all the other bits in the byte (those that do not contain encoded data). The code to do this is as follows.

byte[] TempByteArray = new byte[1];
SourceA.CopyTo(TempByteArray, 0);
BitArray TargetRed = new BitArray(new byte[] { TempByteArray[0] });
BitArray bytRedClearByteInverse = new BitArray(new byte[] { (byte)(255 - m_bytRedClearByte) });

So here we've copied the red byte into a new bit array. Next we take the value that we used to clear out the bits to encode and subtract it from 255. This will provide us a with a bit pattern that is it's inverse. Where m_bytRedClearByte holds the bit pattern 0000 0111 it's inverse has the pattern 1111 1000. This is exactly the pattern we need to clear out those bits that do not contain encoded data in our red byte. We do this by first applying the inverse bit pattern to red byte with a logical "or" and then immediately applying the same inverse pattern to the target red byte with a logical "XOR". As before, a concrete example should make this more clear. For our example DecodeByte receives a byte array with the values red=43, green=91, and blue=90. The bit patterns are below.

0010 1011 red
0101 1011 green
0101 1010 blue

As stated we will use the inverse of the red mask to zero out the bits that do not contain encoded data. We do this by first applying a logical "or".

0010 1011
1111 1000
1111 1011

We then XOR the result

1111 1011
1111 1000
0000 0011

We now know the first three bits of our decoded value is 011

We then follow suit for the green and blue bits. Again our green mask will be the inverse of the mask used to encode. Our green mask, m_bytGreenClearByte, in this example has the exact same bit pattern as the red. The pattern is 0010 1011 and so it's inverse will be 1111 1000. The green pixel byte has a value of 91 and the corresponding bit pattern is then 0101 1011. The binary math is then as follows.

0101 1011
1111 1000
1111 1011

1111 1011
1111 1000
0000 0011

Coincedentally the second three bits are the same as the first. 011.

Finally we come to the blue byte. It's value is 90 and has a bit pattern of 0101 1010. The blue mask, m_bytBlueClearByte, in this example holds a bit pattern of 0000 0011 so its inverse is 1111 1100. We then apply this bit pattern in the same manner to extract the last two bits of the byte to decode.

0101 1010
1111 1100
1111 1110

1111 1110
1111 1100
0000 0010

Our final two bits are 10. Our final decoded bit pattern is therefore 0110 1110 or a value of 110 (that's one hundred and ten). We're not quite done yet though. Our extracted bits are all occupying the least significant bits of the bit arrays they have been extracted to. We need to shift them into their correct positions. Fortunately, we've already calculated the required shift values. These are the same values we used to encode the bytes but now we'll be shifting in the other direction, towards the most significant bit. The code to shift the red bits is below.

byte[] DecodedArray = new byte[1];
TargetRed.CopyTo(DecodedArray, 0);
DecodedArray[0] = (byte)(DecodedArray[0] << m_bytRedShift);
TargetRed = new BitArray(new byte[] { DecodedArray[0] });

Very nearly identical to the encoding shift excepting the direction of the shift. The shift of the green bits again mimics the encoding shift except for the direction of the shift.

TargetGreen.CopyTo(DecodedArray, 0);
DecodedArray[0] = (byte)(DecodedArray[0] << m_bytGreenShift);
TargetGreen = new BitArray(new byte[] { DecodedArray[0] });

And just like the encoding, blue bits are already in their correct position and don't require a shift. Gotta love those blue bits.

Summary - Usage, caveats, next steps

To summarize, what I've covered here is the core of the attached Steganography application. I've gone over how we can encode a file by breaking apart it's component bytes into it's bits and then spreading those bits out in a consistent manner replacing "less than critical" bits in the cover file. I've discussed the importance of encoding meta-data along with our embedded hidden data. How without the use of meta-data the encoding process largely becomes a one way trip for the embedded data. Finally, I detailed how we can reverse the encoded data out and extract and restore the embedded contents. I've chosen to use bitmap files for this tutorial because of the ease with which many of the bitmap forms lend themselves to the task. They are rich in data and it's easy to borrow bits without materially impairing the visible content. With some re-working of the code the visible impact could be further reduced but of course the trade off there is that less data can then be encoded in a given file. The important point is however to demonstrate the concept. That if we can determine a pattern of bits that are less than critical to file (or data stream) integrity then we have opportunity to use those bits to carry a secondary unseen stream of data.

I would like to take a moment to underscore that Steganography is not encryption. Steganography is the practice of hiding data by obscuring the presence of the data. Encryption secures the data by attempting to make it unreadable to unauthorized parties regardless if the presence of the data is know or not. In a Steganography application once the presence of the data has become known it may be relatively easy to then extract the content. In the attached sample application if a copy of the original bitmap is made available for comparison it then becomes fairly easy to extract the hidden content. A one of a kind image generated on the fly would help to make the encoded less accessible. Encoding the bits in reverse order (supported in the attached sample application) or in a pattern inconsistent with the original bit pattern would also make the encoded more secure. In the end a combination of encryption and Steganography would provide the best security.

Enhancements that would improve the application would be built in support for generating bitmap patterns, adding of support for compression and multiple files, and finally linking in support for encrypting the data as it is encoded.

The attached sample application comes with a number of commandline options. Running the executable with no options will display a basic help screen. Here I will take a moment to highlight the options and provide a few sample commandline examples. All options begin with a hyphen followed by the options value.

  • -cmd

    Action to perform. Valid values include encode, decode, info, and size. Info displays information on any embedded file (size and name). Size will display the required cover file size needed to embed a file.

  • -if

    The image file to use in a requested operation (encode, decode, info).

  • -sf

    The source file to encode. When used with "size" this will be the file used to calculate the required bitmap size.

  • -ifo

    Image file to create when encoding a source file into an image.

  • -df

    Name of file to create when decoding a source file from an image.

  • -ifd

    Directory with image files to process (use with "decode" and "info"). This is a limited form of batch processing.

  • -ord

    Order to encode the source file. f for forward or r for reverse. Default is forward.

  • -ppb

    Number of pixels to use to encode each byte. While a given byte is still encoded into a single pixel this allows for the encoding to be spaced out. So that the encoding is not all loaded into the from of the bitmap. Default is 1.

  • -msk

    Bit mask. Identifies contiguous blocks of bits to encode in each color. Three integer values separated by colons. Red:Green:Blue Bit mask values must total to 8. Default bit mask is 3:3:2. This option is more to allow the user of the application to see where the bits have been encoded. A value of 8 for example will show quite clearly where the encoded data resides in the processed bitmap.

  • -run

    Upon completion of decoding the extracted file will be run with the application specified.

Note: When using -ord, -msk, or -ppb the same settings must be used to decode or view the encoded files info. -ord, -msk, and -ppb may be used together or separately.

Example commandlines:

Stegan -cmd encode -if test.bmp -sf d.txt -ifo encoded.bmp

Encodes the file "d.txt" using the file "test.bmp" producing the file "encoded.bmp".

Stegan -cmd encode -if test.bmp -sf d.txt -ifo encoded.bmp -ppb 10

Same as above but takes ten pixels to encode a byte.

Stegan -cmd encode -if test.bmp -sf d.txt -ifo encoded.bmp -msk 8

Same as the first option but encodes all the bits into first color of the pixel.

Stegan -cmd encode -if test.bmp -sf d.txt -ifo encoded.bmp -ord r

Same as first option but encodes data in the reverse order it exists in source file

Stegan -cmd info -if encoded.bmp

Displays the name and size of any file that has been encoded in the file encoded.bmp.

Stegan -cmd decode -if encoded.bmp

Decodes and extracts any file embedded in the file encoded.bmp.

Stegan -cmd decode -if encoded.bmp -df data.txt

Decodes any file embedded in the file encoded.bmp naming the extracted file "data.txt".


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


About the Author

Phoenix Roberts
President Phoenix Roberts LLC
United States United States
We learn a subject by doing but we gain a real understanding of it when we teach others about it.

Comments and Discussions

GeneralMy vote of 5 Pin
tumbledDown2earth9-Apr-13 0:32
membertumbledDown2earth9-Apr-13 0:32 
SuggestionVote of 5... but Pin
MarkJoel6030-Nov-12 4:18
memberMarkJoel6030-Nov-12 4:18 
GeneralRe: Vote of 5... but Pin
embix21-Oct-13 10:06
memberembix21-Oct-13 10:06 
GeneralMy vote of 5 Pin
Shine Jacob (Enot)30-Aug-12 21:27
memberShine Jacob (Enot)30-Aug-12 21:27 
QuestionReally good one but can be done simply Pin
Shine Jacob (Enot)30-Aug-12 21:26
memberShine Jacob (Enot)30-Aug-12 21:26 
QuestionSpread Spectrum Algorithm Pin
Igi Shellshock26-Jun-12 19:02
memberIgi Shellshock26-Jun-12 19:02 
GeneralMy vote of 5 Pin
Filip D'haene15-May-12 7:55
memberFilip D'haene15-May-12 7:55 
QuestionNice Pin
Sacha Barber13-Apr-12 7:26
mvpSacha Barber13-Apr-12 7:26 
AnswerRe: Nice Pin
Phoenix Roberts17-Apr-12 4:16
memberPhoenix Roberts17-Apr-12 4:16 
QuestionNicely explained. Pin
mark merrens7-Apr-12 6:03
membermark merrens7-Apr-12 6:03 
GeneralMy vote of 5 Pin
manoj kumar choubey2-Apr-12 4:56
membermanoj kumar choubey2-Apr-12 4:56 
QuestionPlain site? Pin
Sc3pt1c4l2-Apr-12 2:47
memberSc3pt1c4l2-Apr-12 2:47 
GeneralRe: Plain site? Pin
Paul @ The Computer Station2-Apr-12 4:21
groupPaul @ The Computer Station2-Apr-12 4:21 
AnswerRe: Plain site? Pin
Taset2-Apr-12 6:55
memberTaset2-Apr-12 6:55 
GeneralRe: Plain site? Pin
Phoenix Roberts2-Apr-12 8:02
memberPhoenix Roberts2-Apr-12 8:02 
GeneralMy vote of 5 Pin
jtrz28-Mar-12 12:12
memberjtrz28-Mar-12 12:12 
QuestionWhat About . . . Pin
W∴ Balboos28-Mar-12 4:06
memberW∴ Balboos28-Mar-12 4:06 
AnswerRe: What About . . . Pin
Phoenix Roberts28-Mar-12 9:37
memberPhoenix Roberts28-Mar-12 9:37 
GeneralMy vote of 5 Pin
Tom Delany27-Mar-12 13:14
memberTom Delany27-Mar-12 13:14 

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 | Terms of Use | Mobile
Web04 | 2.8.150428.2 | Last Updated 7 Apr 2012
Article Copyright 2012 by Phoenix Roberts
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid