Torrentfile-implementation under wx

If you have a cool piece of software to share, but you are not hosting it officially yet, please dump it in here. If you have code snippets that are useful, please donate!
Post Reply
KaReL
Experienced Solver
Experienced Solver
Posts: 78
Joined: Mon Aug 30, 2004 8:52 am
Contact:

Torrentfile-implementation under wx

Post by KaReL »

TorrentFile.h:

Code: Select all

#ifndef __TORRENT_FILE_H
#define __TORRENT_FILE_H

#include <wx/wfstream.h>
#include <wxMap.h>
struct SHA_buf
{
  SHA_buf( )                      { memset(buf, 0    , 20); }
  SHA_buf( BYTE const * const b ) { memcpy(buf, b    , 20); }
  SHA_buf( const SHA_buf& s )     { memcpy(buf, s.buf, 20); }

  BYTE buf[20];
};

class TorrentPiece
{
public:
  typedef enum eTorrentKind
  {
    torrent_none,

    torrent_string,
    torrent_integer,
    torrent_dictionary,
    torrent_list,
    torrent_sha1,
  } eTorrentKind;

  TorrentPiece( eTorrentKind kind = torrent_none );
  TorrentPiece( const TorrentPiece& p );
  virtual ~TorrentPiece( ) { }

  virtual bool bDecode( wxInputStream& input_stream ) = 0;
  virtual void bEncode( wxOutputStream& output_stream ) const = 0;

  virtual void display_nice( wxOutputStream& output ) const = 0;

  bool IsKindOf( eTorrentKind kind ) const { return torrent_kind == kind; }

  bool IsOk( ) const { return m_bOK; }

protected:
  bool m_bOK;

private:
  eTorrentKind torrent_kind;
};
WX_DECLARE_STRING_MAP (TorrentPiece*, string_string   );
WX_DECLARE_BASEARRAY  (TorrentPiece*, torrent_listing );
WX_DECLARE_BASEARRAY  (SHA_buf      , SHA_pieces      );

class TorrentSHA1 : public TorrentPiece
{
public:
  TorrentSHA1( );
  TorrentSHA1( wxInputStream& input_stream );
  TorrentSHA1( const TorrentSHA1& l );
  virtual ~TorrentSHA1( ) { }

  virtual bool bDecode( wxInputStream& input_stream );
  virtual void bEncode( wxOutputStream& output_stream ) const;

  virtual void display_nice( wxOutputStream& output ) const;

  const SHA_pieces& GetValue( ) const { return value; }

private:
  SHA_pieces value;
};

class TorrentInteger : public TorrentPiece
{
public:
  TorrentInteger( );
  TorrentInteger( wxInputStream& input_stream );
  TorrentInteger( double l );
  TorrentInteger( const TorrentInteger& l );
  virtual ~TorrentInteger( ) { }

  operator double( ) const { return GetValue(); }

  virtual bool bDecode( wxInputStream& input_stream );
  virtual void bEncode( wxOutputStream& output_stream ) const;

  virtual void display_nice( wxOutputStream& output ) const;

  double GetValue( ) const { return value; }

private:
  double value;
};

class TorrentString : public TorrentPiece
{
public:
  TorrentString( );
  TorrentString( wxInputStream& input_stream );
  TorrentString( const wxString& str );
  TorrentString( const TorrentString& l );
  virtual ~TorrentString( ) { }

  operator wxString() const { return GetValue(); }
  bool operator ==( const wxString& s ) const { return GetValue() == s; }

  virtual bool bDecode( wxInputStream& input_stream );
  virtual void bEncode( wxOutputStream& output_stream ) const;

  virtual void display_nice( wxOutputStream& output ) const;

  const wxString& GetValue( ) const { return value; }
  double length() const { return GetValue().length(); }

private:
  wxString value;
};

class TorrentDictionary : public TorrentPiece
{
public:
  TorrentDictionary( );
  TorrentDictionary( wxInputStream& input_stream );
  TorrentDictionary( const TorrentDictionary& l );
  virtual ~TorrentDictionary( );

  virtual bool bDecode( wxInputStream& input_stream );
  virtual void bEncode( wxOutputStream& output_stream ) const;

  virtual void display_nice( wxOutputStream& output ) const;

  const string_string& GetValue( ) const { return value; }

private:
  string_string value;
};

class TorrentList : public TorrentPiece
{
public:
  TorrentList( );
  TorrentList( wxInputStream& input_stream );
  TorrentList( const TorrentList& l );
  virtual ~TorrentList( );
 
  virtual bool bDecode( wxInputStream& input_stream );
  virtual void bEncode( wxOutputStream& output_stream ) const;

  virtual void display_nice( wxOutputStream& output ) const;

  const torrent_listing& GetValue( ) const { return value; }

private:
  torrent_listing value;
};



class TorrentFile
{
public:
  TorrentFile( )                             : piece(NULL)  { }
  TorrentFile( const wxString& file_name );
  TorrentFile( wxInputStream& input_stream ) : piece(NULL)  { bDecode(input_stream); }
 ~TorrentFile( )                                            { wxDELETE(piece); }

  bool bDecode( wxInputStream& input_stream );
  void bEncode( wxOutputStream& output_stream ) const;

  void display_nice( wxOutputStream& output ) const { piece->display_nice(output); }
  void display_nice( const wxString& filename ) const { wxFileOutputStream output(filename); piece->display_nice(output); }

  wxString GetInfoHash( ) const;
  wxString GetAnnounceURL( ) const;
  wxString GetName( ) const;
  wxString GetError( ) const;
  
  double GetTotalLength( ) const;
  double GetTotalFiles( ) const;

  bool IsOk() const { return piece != NULL && piece->IsOk(); }

private:
  TorrentPiece * findInTorrent( const wxString& key, TorrentPiece const * const mPiece = NULL ) const;

  TorrentPiece * piece;
};

#endif // __TORRENT_FILE_H
TorrentFile.cpp:

Code: Select all

#include "stdwx.h"
#include "TorrentFile.h"
#include <encryption/wxSHA1.h>
#include <wx/variant.h>
#include <wx/mstream.h>

size_t inspring = 0;

wxOutputStream& operator <<(wxOutputStream& o_stream, const wxString& s)
{
  o_stream.Write( s.mbc_str(), strlen(s.mbc_str()) );
  return o_stream;
}

wxOutputStream& operator <<(wxOutputStream& o_stream, long s)
{
  wxString c( wxVariant(s).GetString() );

  o_stream.Write( c.mbc_str(), strlen(c.mbc_str()) );

  return o_stream;
}

TorrentPiece * getNextPiece( wxInputStream& input_stream )
{
  TorrentPiece * piece;

  char c = input_stream.Peek();
  switch( c )
  {
  case 'd': // dictionary
    input_stream.GetC();
    piece = new TorrentDictionary(input_stream);
    break;

  case 'i': // integer
    input_stream.GetC();
    piece = new TorrentInteger(input_stream);
    break;

  case 'l': // list
    input_stream.GetC();
    piece = new TorrentList(input_stream);
    break;

  default:  // string
    piece = new TorrentString(input_stream);
    break;
  }

  if ( piece && !piece->IsOk() )
    wxDELETE(piece);
  
  return piece;
}

bool shouldStopToRead( wxInputStream& input_stream )
{
  return ( input_stream.Eof() || input_stream.Peek() == 'e' );
}

//////////////////////////////////////////////////////////////////////////
// TorrentPiece
TorrentPiece::TorrentPiece( eTorrentKind kind )
: torrent_kind(kind),
  m_bOK(false)
{
  wxASSERT(torrent_kind != torrent_none);
}

TorrentPiece::TorrentPiece( const TorrentPiece& p )
: torrent_kind(p.torrent_kind),
  m_bOK(p.m_bOK)
{
  wxASSERT(torrent_kind != torrent_none);
}

//////////////////////////////////////////////////////////////////////////
// TorrentSHA1
TorrentSHA1::TorrentSHA1( )
: TorrentPiece(torrent_sha1)
{
}

TorrentSHA1::TorrentSHA1( wxInputStream& input_stream )
: TorrentPiece(torrent_sha1)
{
  bDecode(input_stream);
}

TorrentSHA1::TorrentSHA1( const TorrentSHA1& l )
: TorrentPiece(l),
  value(l.value)
{
}

bool TorrentSHA1::bDecode( wxInputStream& input_stream )
{ // get the amount of data in it...
  wxString amount;
  do
  {
  	char c = input_stream.GetC();
    if ( c == ':' )
      break;
    if ( !wxIsdigit(c) )
      return false;
    amount << wxString(&c, wxConvLibc, sizeof(char));
  } while( !shouldStopToRead(input_stream) );

  long am = wxVariant(amount).GetLong();
  wxASSERT( am % 20 == 0 ); // check if there is the certain amount of data in this segment

  BYTE buf[20];
  for ( int i = 0; i < am; i += 20 )
  {
    input_stream.Read( buf, 20 );
    value.push_back( buf );
  }

  m_bOK = true;
  return true;
}

void TorrentSHA1::bEncode( wxOutputStream& output_stream ) const
{
  if ( !IsOk() )
    return;

  output_stream << (value.size() * 20) << wxT(":");
  
  for ( SHA_pieces::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    // directly write to outputstream.
    output_stream.Write( (*ci).buf, 20 );
  }

}

void TorrentSHA1::display_nice( wxOutputStream& output ) const
{
  if ( !IsOk() )
    return;

  for ( SHA_pieces::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    BYTE const * const tmp = (*ci).buf;

    output << wxT("* ");
    for ( int i = 0; i < 20; i++ )
    {
      output << wxString::Format( wxT("%02X"), tmp[i] );
      if ( (i+1) % 4 == 0 )
        output << wxT(" ");
    }
    output << wxT(",\n") << wxString( wxT(' '), inspring );
  }
}

//////////////////////////////////////////////////////////////////////////
// TorrentList
TorrentList::TorrentList( )
: TorrentPiece(torrent_list)
{
}

TorrentList::TorrentList( wxInputStream& input_stream )
: TorrentPiece(torrent_list)
{
  bDecode(input_stream);
}

TorrentList::TorrentList( const TorrentList& l )
: TorrentPiece(l),
  value(l.value)
{ }

TorrentList::~TorrentList( )
{
  for ( torrent_listing::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    TorrentPiece * piece = *ci;
    wxDELETE(piece);
  }

  value.clear();
}

bool TorrentList::bDecode( wxInputStream& input_stream )
{
  while ( !shouldStopToRead(input_stream) )
  {
    TorrentPiece * piece = getNextPiece(input_stream);
    if ( piece == NULL || !piece->IsOk() )
    {
      wxDELETE(piece);
      return false;
    }

    value.push_back( piece );
  }

  // eat 1 'e'
  input_stream.GetC();
  m_bOK = true;
  return true;
}

void TorrentList::bEncode( wxOutputStream& output_stream ) const
{
  if ( !IsOk() )
    return;

  output_stream << wxT("l");
  for ( torrent_listing::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    (*ci)->bEncode(output_stream);
  }
  output_stream << wxT("e");
}

void TorrentList::display_nice( wxOutputStream& output ) const
{
  if ( !IsOk() )
    return;

  output << wxT("[\n");

  inspring += 1;

  for ( torrent_listing::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    TorrentPiece * piece = *ci;

    output << wxString(wxT(' '), inspring);
    piece->display_nice(output);

    output << wxT(",\n");
  }

  inspring -= 1;

  output << wxString(wxT(' '), inspring) << wxT("]");
}

//////////////////////////////////////////////////////////////////////////
// TorrentDictionary
TorrentDictionary::TorrentDictionary( )
: TorrentPiece(torrent_dictionary)
{
}

TorrentDictionary::TorrentDictionary( wxInputStream& input_stream )
: TorrentPiece(torrent_dictionary)
{
  bDecode(input_stream);
}

TorrentDictionary::TorrentDictionary( const TorrentDictionary& l )
: TorrentPiece(l),
  value(l.value)
{ }

TorrentDictionary::~TorrentDictionary( )
{
  for ( string_string::iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    TorrentPiece * piece = (*ci).second;
    wxDELETE(piece);
  }

  value.clear();
}

bool TorrentDictionary::bDecode( wxInputStream& input_stream )
{
  do
  {
  	TorrentString key(input_stream);

    if ( !key.IsOk() )
      return false;

    TorrentPiece * piece = NULL;
    if ( key == wxT("pieces") )
    {
      // Now modify the piece[info][pieces]-string value to TorrentSHA1 values...
      piece = new TorrentSHA1(input_stream);
    }
    else
    {
      piece = getNextPiece(input_stream);
    }

    if ( piece == NULL || !piece->IsOk() )
    {
      wxDELETE(piece);
      return false;
    }

    value[key] = piece;
  } while( !shouldStopToRead(input_stream) );

  // eat 1 'e'
  input_stream.GetC();

  m_bOK = true;
  return true;
}

void TorrentDictionary::bEncode( wxOutputStream& output_stream ) const
{
  if ( !IsOk() )
    return;

  output_stream << wxT("d");

  for ( string_string::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    TorrentString((*ci).first).bEncode(output_stream);
    (*ci).second->             bEncode(output_stream);
  }

  output_stream << wxT("e");
}

void TorrentDictionary::display_nice( wxOutputStream& output ) const
{
  if ( !IsOk() )
    return;

  // write the first of the string
  output << wxT("{\n");

  inspring += 1;

  for ( string_string::const_iterator ci = value.begin(); ci != value.end(); ci++ )
  {
    // write the key-value.
    const TorrentString val = TorrentString((*ci).first);
    TorrentPiece const * const piece = (*ci).second;

    output << wxString(wxT(' '), inspring);
    val.display_nice(output);

    inspring += (*ci).first.length() + 2 /* "x2 */ + 3;
      output << wxT(" : ");
      piece->display_nice(output);
    inspring -= ((*ci).first.length() + 2 /* "x2 */ + 3);

    output << wxT(",\n");
  }

  inspring -= 1;

  output << wxString(wxT(' '), inspring) << wxT("}");
}

//////////////////////////////////////////////////////////////////////////
// TorrentInteger
TorrentInteger::TorrentInteger( )
: TorrentPiece(torrent_integer)
{
}

TorrentInteger::TorrentInteger( double l )
: TorrentPiece(torrent_integer),
  value(l)
{
  m_bOK = true;
}

TorrentInteger::TorrentInteger( wxInputStream& input_stream )
: TorrentPiece(torrent_integer)
{
  bDecode(input_stream);
}

TorrentInteger::TorrentInteger( const TorrentInteger& l )
: TorrentPiece(l),
  value(l.value)
{ }

bool TorrentInteger::bDecode( wxInputStream& input_stream )
{
  wxString integer;
  while( !shouldStopToRead(input_stream) )
  {
    char c = input_stream.GetC();

    if ( !wxIsdigit(c) )
      return false;

    integer << wxString(&c, wxConvLibc, sizeof(char));
  }

  // remove the 'e'
  input_stream.GetC();

  value = wxVariant(integer).GetLong();

  m_bOK = true;
  return true;
}

void TorrentInteger::bEncode( wxOutputStream& output_stream ) const
{
  if ( !IsOk() )
    return;

  output_stream << wxT("i") << wxString::Format(wxT("%.f"),value) << wxT("e");
}

void TorrentInteger::display_nice( wxOutputStream& output ) const
{
  if ( !IsOk() )
    return;

  output << wxString::Format( wxT("%.f"), value );
}

//////////////////////////////////////////////////////////////////////////
// TorrentString
TorrentString::TorrentString( )
: TorrentPiece(torrent_string)
{
}

TorrentString::TorrentString( wxInputStream& input_stream )
: TorrentPiece(torrent_string)
{
  bDecode(input_stream);
}

TorrentString::TorrentString( const wxString& str )
: TorrentPiece(torrent_string),
  value(str)
{
  m_bOK = true;
}

TorrentString::TorrentString( const TorrentString& l )
: TorrentPiece(l),
  value(l.value)
{ }

bool TorrentString::bDecode( wxInputStream& input_stream )
{
  wxString amount;
  do
  {
  	char c = input_stream.GetC();
    if ( c == ':' )
      break;
    else if( !wxIsdigit(c) )
      return false;
    amount << wxString(&c, wxConvLibc, sizeof(char));
  } while( !input_stream.Eof() );

  // lees nu 'amount' bytes in.
  double am = wxVariant(amount).GetLong();
  char * t = new char[ am ];
  input_stream.Read( t, am );

  value = wxString(t, wxConvLibc, am);

  wxDELETEA(t);

  m_bOK = true;
  return true;
}

void TorrentString::bEncode( wxOutputStream& output_stream ) const
{
  if ( !IsOk() )
    return;

  output_stream << strlen(value.mbc_str().data()) << wxT(":") << value;
}

void TorrentString::display_nice( wxOutputStream& output ) const
{
  if ( !IsOk() )
    return;

  output << wxString::Format( wxT("\"%s\""), value.c_str() );
}

//////////////////////////////////////////////////////////////////////////
// TorrentFile
TorrentFile::TorrentFile( const wxString& file_name )
: piece(NULL)
{
  if ( wxFileExists(file_name) )
  {
    wxFileInputStream input_stream(file_name);
    bDecode(input_stream);
  }
}

bool TorrentFile::bDecode( wxInputStream& input_stream )
{
  return (piece = getNextPiece(input_stream)) != NULL;
}

void TorrentFile::bEncode( wxOutputStream& output_stream ) const
{
  piece->bEncode(output_stream);
}

wxString TorrentFile::GetInfoHash( ) const
{
  TorrentPiece * piece = findInTorrent( wxT("info") );

  if ( piece == NULL )
    return wxEmptyString;

  wxMemoryOutputStream out_stream;
  piece->bEncode( out_stream );
  BYTE * len = new BYTE[out_stream.GetLength()];
  out_stream.CopyTo(len, out_stream.GetLength());

  wxSHA1 h;
  wxString retVal;

  if ( h.HashBuffer(len, out_stream.GetLength()) )
  {
    for ( int i = 0; i < 20; i++ )
    {
      retVal << wxString::Format( wxT("%%%02X"), h.GetHash()[i] );
    }
  }

  wxDELETEA(len);
  return retVal;
}

TorrentPiece * TorrentFile::findInTorrent( const wxString& key, TorrentPiece const * const mPiece /* = NULL */ ) const
{
  TorrentPiece const * working_piece = mPiece == NULL ? piece : mPiece;

  if ( working_piece == NULL || !working_piece->IsKindOf(TorrentPiece::torrent_dictionary) )
    return NULL;

  TorrentDictionary const * dict = reinterpret_cast<TorrentDictionary const *>(working_piece);

  string_string::const_iterator ci = dict->GetValue().find(key);

  return ci == dict->GetValue().end() ? NULL : (*ci).second;
}

double TorrentFile::GetTotalLength( ) const
{
  TorrentPiece * dict = findInTorrent(wxT("info"));
  if ( dict == NULL ) return 0;

  TorrentList * lijst = reinterpret_cast<TorrentList*>(findInTorrent( wxT("files"), dict ));
  if ( lijst == NULL )
  {
    TorrentInteger * integer = reinterpret_cast<TorrentInteger*>(findInTorrent( wxT("length"), dict ));
    if ( integer == NULL )
      return 0;
    else
      return integer->operator double();
  }

  double total_size(0);
  for ( torrent_listing::const_iterator ci = lijst->GetValue().begin(); ci != lijst->GetValue().end(); ci++ )
  {
    TorrentInteger * filesize = reinterpret_cast<TorrentInteger*>(findInTorrent( wxT("length"), *ci ));
    if ( filesize )
      total_size += filesize->GetValue();
  }

  return total_size;
}

wxString TorrentFile::GetName( ) const
{
  TorrentPiece * dict = findInTorrent(wxT("info"));
  if ( dict == NULL ) return wxEmptyString;

  TorrentPiece * piece = findInTorrent(wxT("name"), dict);
  if ( piece == NULL )
    return wxEmptyString;
  else
    return *(reinterpret_cast<TorrentString*>(piece));
}

wxString TorrentFile::GetAnnounceURL( ) const
{
  TorrentPiece * piece = findInTorrent( wxT("announce") );
  if ( piece == NULL )
    return wxEmptyString;
  else
    return *(reinterpret_cast<TorrentString*>(piece));
}

double TorrentFile::GetTotalFiles( ) const
{
  TorrentPiece * dict = findInTorrent(wxT("info"));
  if ( dict == NULL ) return 0;

  TorrentList * lijst = reinterpret_cast<TorrentList*>(findInTorrent(wxT("files"), dict));
  if ( lijst == NULL )
    return findInTorrent(wxT("length"), dict) == NULL ? 0 : 1;

  return lijst->GetValue().size();
}

wxString TorrentFile::GetError( ) const
{
  TorrentPiece * piece = findInTorrent( wxT("failure reason") );
  if ( piece == NULL || !piece->IsOk() ) return wxEmptyString;

  return *(reinterpret_cast<TorrentString*>(piece));
}
I was bored a little so I analysed the bittorrent protocol and played a little with it...
I think someone can put this to some good use I am sure :p.

Questions: [email protected]
Flames: /dev/null

Peace, out.
Last edited by KaReL on Fri Jun 17, 2005 6:16 pm, edited 3 times in total.
wxWidgets: SVN/trunk
OS: WinXP/2 + Ubuntu + Mac 10.4.11
Compiler: VS2005 + GCC 4.2 + GCC 4.0.1
-----
home: http://www.salvania.be
geon
I live to help wx-kind
I live to help wx-kind
Posts: 189
Joined: Tue Sep 07, 2004 4:10 pm
Location: Sweden, Uppsala

Post by geon »

I had problem following your code. Some comments could help. Also, the usage is pretty unclear.
KaReL
Experienced Solver
Experienced Solver
Posts: 78
Joined: Mon Aug 30, 2004 8:52 am
Contact:

Post by KaReL »

14/06/2005
- Added the bittorrent SHA1-implementation of the segments.
- Added the possibility to write output starting from the TorrentFile-class.
15/06/2005
- Added the possibility to en/decode files on the fly.
17/06/2005
- Get the InfoHash. This is needed for communicating to the tracker.
- Get the total amount of files + their total file length.
- implemented everything with doubles instead of longs as I encountered torrents which were larger then a "long" :p.
- Also implemented convenience functions like IsOk() and stuff...

The usage is pretty straightforward:

Code: Select all

  wxFileInputStream input( wxT("/tmp/just.a.torrent") );
  TorrentFile tf( input );

  wxFileOutputStream output( wxT("/tmp/debug_output") );
  tf.display_nice(output);

  wxFileOutputStream encode_test( wxT("/tmp/output.torrent") );
  tf.bEncode(encode_test);
This opens a torrent, parses it, and prints it out in a nice way.
Last edited by KaReL on Fri Jun 17, 2005 6:21 pm, edited 1 time in total.
wxWidgets: SVN/trunk
OS: WinXP/2 + Ubuntu + Mac 10.4.11
Compiler: VS2005 + GCC 4.2 + GCC 4.0.1
-----
home: http://www.salvania.be
geon
I live to help wx-kind
I live to help wx-kind
Posts: 189
Joined: Tue Sep 07, 2004 4:10 pm
Location: Sweden, Uppsala

Post by geon »

So it doesn't actually download the file specified by the .torrent?
User avatar
Ryan Norton
wxWorld Domination!
wxWorld Domination!
Posts: 1319
Joined: Mon Aug 30, 2004 6:01 pm

Post by Ryan Norton »

Havn't tried it yet but it looks sweet!

You can use wxFileSystem to download it like

Code: Select all

wxFSFile* pTheFile = wxFileSystem::OpenFile(torrentpath);

//do some processing via pTheFile->GetInputStream()

delete pTheFile;

Should work with local paths, if not try

Code: Select all

wxURI torrenturi(torrentpath);

if(torrenturi.HasScheme() == false)
{
    //local file
    torrentpath = wxString(wxT("file://")) + torrentpath;
}
before that.
[Mostly retired moderator, still check in to clean up some stuff]
KaReL
Experienced Solver
Experienced Solver
Posts: 78
Joined: Mon Aug 30, 2004 8:52 am
Contact:

Post by KaReL »

geon wrote:So it doesn't actually download the file specified by the .torrent?
No, you need to do the downloading yourself... The only thing this class does is make it more easy to work with .torrents. That's it.

This class can even be used as a baseclass for TnGBc (The next Generation Bittorrent client). Because all the rest of the stuff is connecting/disconnecting from people (sockets) and such, but that's really easy :p (this class too, but it's even more easy if you just can c/p).

BTW, changes updated.
wxWidgets: SVN/trunk
OS: WinXP/2 + Ubuntu + Mac 10.4.11
Compiler: VS2005 + GCC 4.2 + GCC 4.0.1
-----
home: http://www.salvania.be
Post Reply