Home Upgrade Search Memberlist Extras Hacker Tools Award Goals Help Wiki Contact

C 

Complexity: Introduction to the Win32 API



Introduction to the Win32 API
[Image: MCGSCNZ.gif]

Hello and welcome to my tutorial on the Windows Application Programming Interface for C++. So what is it? The Windows API is a way for programmers to create graphical user interfaces (GUI) on the Windows operating system (OS). It allows the programmer to have a simpler way to do this rather than have to handle compatibility with different hardware and various other aspects of development between machines.

So why use the WinAPI? Aren't there easier packages to do this?

This is true, various frameworks such as MFC and .NET exist for rapid development of windows forms. Most of these frameworks work as wrappers for the Win32 API, where all of the code used would be wrapped within premade classes that cuts down on the amount of code that must be wrote.

The Win32 API should still be learned though as it can be helpful in the event of an error with one of these frameworks. With no understanding of how these frameworks really work, it will be difficult or more time consuming to fix any problems.

As this is my first tutorial on the Win32 API, I'm only going to show the Windows version of the popular Hello World program and explain it step by step. Because a bit more work would go into using the WinAPI, the Hello World program will be much bigger than expected.

This is a simple "Hello World" program using regular C++:

PHP Code

<?php 
#include <iostream>
using namespace std;

int main() {
  
cout << "Hello, World!";
  return 
0;
} 

This is a nice short program. In contrast, the Win32 requires much more code just for a simple window. Charles Petzold (who has wrote many books on programming with the Windows API) has been quoted as saying "The original hello world program in the Windows 1.0 SDK was a bit of a scandal. HELLO.C was about 150 lines long, and the HELLO.RC resource script had another 20 or so more lines. (...) Veteran C programmers often curled up in horror or laughter when encountering the Windows hello-world program".

Okay, so now onto our program. The first thing to do before anything else is done, is include the Windows header file, as this is the file where we get all the Windows functions, data structures, etc. This is done using something called a "preprocessor directive". This is a command that the compiler reads and responds to, in this case it bundles the windows header file along with your source into the program. Preprocessor directives are preceeded by a hash (#) symbol. Unlike other statements in C++, it is not ended by a semicolon (;).

PHP Code

<?php 
#include <Windows.h> 

The next thing to do is create our main function. Console based programs in C++ will use an actual main() function, but when programming with Win32 you use either WinMain() or wWinMain() as Windows.h will provide its own version of the main function. The only difference between the two is wWinMain uses unicode and thus wide characters, whereas WinMain used ASCII character codes.

In this tutorial I will use the wWinMain() function. It has the following prototype:

PHP Code

<?php 
int WINAPI wWinMain
(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow); 

hInstance is the handle to the windows instance. prevInstance is also handle to a windows instance, but it should always be 0 as it is a parameter only used on 16 bit Windows. cmdLine is the arguments given to the program as a unicode string if the program was run from the command prompt. cmdShow is a flag that tells Windows if the program should start maximised, minimised or normal.

Here is our empty wWinMain function:

PHP Code

<?php 
#include <Windows.h>

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow) {
  return 
static_cast<int>(msg.wParam);
} 

Because our program won't actually be using prevInstance or cmdLine, two additional functions can be called inside wWinMain which stops the compiler throwing errors at you for not using arguments.

PHP Code

<?php 
#include <Windows.h>

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow) {
  
UNREFERENCED_PARAMETER(prevInstance);
  
UNREFERENCED_PARAMETER(cmdLine);
  return 
static_cast<int>(msg.wParam);
} 

Okay, now the next step is to create a windows class. This is done using WNDCLASSEX, a type defintion of the data structure tagWNDCLASSEX. The MSDN documentation defines it as follows:

PHP Code

<?php 
typedef struct tagWNDCLASSEX 
{
  
UINT cbSize; //Size in bytes of the structure
  
UINT style; //Class style flags
  
WNDPROC lpfnWndProc; //A pointer to the window procedure
  
int cbClsExtra; //Number of extra bytes to allocate the window-class structure
  
int cbWndExtra; //The number of extra bytes to allocate the window instance
  
HINSTANCE hInstance; //A handle to the window procedure for the class
  
HICON hIcon; //A handle to the class icon
  
HCURSOR hCursor; //A handle to the class cursor
  
HBRUSH hbrBackground; //A handle to the class background brush
  
LPCTSTR lpszMenuName; Pointer to string that specifies the resource name of the class menu
  LPCTSTR lpszClassName
; //A pointer to a string specifying the class name
  
HICON hIconSm; A handle to a small icon for the window class
} 
WNDCLASSEX, *PWNDCLASSEX; 

To create your new window class, put the following code in your wWinMain function, after the UNREFERENCED_PARAMETER() function and before the return statement.

PHP Code

<?php 
WNDCLASSEX wndClass 
= {0};

wndClass.cbSize = sizeof(WNDCLASSEX);
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WndProc;
wndClass.hInstance = hInstance;
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = "MyTutorialWindow"; 

In this snippet of code, we have created an instance of WNDCLASSEX and called it wndClass, then we initiate all of its values to 0 (0 is equivalent to null in the Windows API, somewhere in Windows.h there is a line reading #define NULL 0).

The size of the structure is set to the size of the WNDCLASSEX structure. Next it is given several style flags, in this case CS_HREDRAW and CS_VREDRAW. Flags are always separated by the bitwise OR symbol. CS_HREDRAW and CS_VREDRAW both redraw the entire window if a change is made to the horizontal or vertical client area respectively. A list of style flags can be found here.

Next lpfnWndProc was set to WndProc, a function that we will be creating later. The hInstance is set to the application instance containing the window procedure for the class. hCursor is set using the LoadCursor function, which takes an HINSTANCE (optional) and LPCTSTR argument which is the name for the cursor to be used. To get a list of all cursor types available, use this link. this function will return a variable of type HCURSOR, which is the handle to the cursor. If the function fails it returns a NULL (0) value.

hbrBackground is the handle to the background brush for this class. It may be a colour value or a handle to a brush. If it is a colour value, it must use one of the following colour values:
  • COLOR_ACTIVEBORDER
  • COLOR_ACTIVECAPTION
  • COLOR_APPWORKSPACE
  • COLOR_BACKGROUND
  • COLOR_BTNFACE
  • COLOR_BTNSHADOW
  • COLOR_BTNTEXT
  • COLOR_CAPTIONTEXT
  • COLOR_GRAYTEXT
  • COLOR_HIGHLIGHT
  • COLOR_HIGHLIGHTTEXT
  • COLOR_INACTIVEBORDER
  • COLOR_INACTIVECAPTION
  • COLOR_MENU
  • COLOR_MENUTEXT
  • COLOR_SCROLLBAR
  • COLOR_WINDOW
  • COLOR_WINDOWFRAME
  • COLOR_WINDOWTEXT

Each of these values is equivalent to an integer. The reason we must use +1 is because the first value is equivalent to 0, so we add 1 so windows doesn't think we are supplying a NULL brush. We have set the menu name to null as we don't have one and we have called our class "MyTutorialWindow". Remember this name, as it will be used again later.

Next we must register this class. Add the following snippet of code after before the return statement.

PHP Code

<?php 
if(!RegisterClassEx(&wndClass)) {
  return -
1;
} 

This if conditional check uses the RegisterClassEx function as it's condition. If RegisterClassEx successfully registers the class it will return true. If it fails it will return false. This statement uses the NOT operator (!), so it checks if RegisterClassEx returns false. If it does, the wWinMain function returns -1 and will terminate.

The next step is to create our window and the handle to it. Add the following code before the return statement.

PHP Code

<?php 
HWND hwnd 
= CreateWindow(
  
"MyTutorialWindow",
  
"Hello, World!",
  
WS_OVERLAPPEDWINDOW,
  
CW_USEDEFAULT,
  
CW_USEDEFAULT,
  
500,
  
300,
  
NULL,
  
NULL,
  
hInstance,
  
NULL
); 

We have created a handle to a window called hwnd, which has been initialised using the CreateWindow() function. This takes 11 parameters. Although this is a function call, you may have noticed I have separated the arguments on different lines. C++ allows the use of whitespace like this, so it should be taken advantage of to improve readability of your code. In the MSDN document, CreateWindow is defined as follows:

PHP Code

<?php 
HWND WINAPI CreateWindow
(
  
LPCTSTR lpClassName,
  
LPCTSTR lpWindowName,
  
DWORD dwStyle,
  
int x,
  
int y,
  
int nWidth,
  
int nHeight,
  
HWND hWndParent,
  
HMENU hMenu,
  
HINSTANCE hInstance,
  
LPVOID lpParam
); 

It has a return value of HWND (Window Handle). It takes the argument of lpClassName (the class name we created earlier), lpWindowName (the text that will appear in the title label), the style flags (a full list can be found here), the initial x and y coordinates (in this case we used the default, CW_USEDEFAULT), the dimensions of the window, the handle of the parent window (ours has no parent so it is NULL), the handle to any menu associated with the window, an instance to the module associated with the window, and a pointer to a value passed through CREATESTRUCT (this isn't necessary for our window so it has also been set to NULL).

After creating a window it is good practice to check if CreateWindow() failed. To do this, add the following snippet.

PHP Code

<?php 
if(!hwnd) {
  return -
1;
} 

If this function is successful it will return an HWND for hwnd, but if it fails hwnd will be NULL which is equivalent to 0. This conditional check sees if hwnd is NULL, if it is the wWinMain function returns -1 and the program terminates. To display our window we now called the ShowWindow() function. This takes 2 arguments, the window handle of the window we want to display, and what state the window should open in.

PHP Code

<?php 
ShowWindow
(hwnd, cmdShow); 

cmdShow will be given by the wWinMain function, it is one of the arguments we gave when writing it. After the first time the window is called and you decide to call ShowWindow again to change the state, the following values can be used instead of cmdShow:
  • SW_HIDE: 0, hides the window and activates another window
  • SW_SHOWNORMAL: 1, show the window in its normal state
  • SW_SHOWMINIMIZED: 2, activates the window and minimises it
  • SW_SHOWMAXIMIZED: 3, maximises the window
  • SW_SHOWNOACTIVATE: 4, similar to SW_SHOWNORMAL but the window is not activated
  • SW_SHOW: 5, activates the window and shows it in its current size and position
  • SW_MINIMIZE: 6, similar to SW_SHOWMINIMIZED but the next window in the Z order is activated
  • SW_SHOWMINNOACTIVE: 7, similar to SW_SJOWMINIMIZED but the window is not activated
  • SW_SHOWNA: 8, similar to SW_SHOW but the window is not activated
  • SW_RESTORE: 9, activates the window and displays it in its original size and position
  • SW_SHOWDEFAULT: 10, sets the window to a default size and position
  • SW_FORCEMINIMIZE: 11, minimises the window even if the thread that owns the window is not responding

To see these calls in action, you could change your ShowWindow call to the following:

PHP Code

<?php 
ShowWindow
(hwnd, cmdShow);
Sleep(2000);
ShowWindow(hwnd, SW_SHOWMAXIMIZED); 

This will wait for 2 seconds, then maximise the window. Don't try this until the entire window is made though, as there are still some parts to add before the window will even work. While following the rest of this tutorial, keep only the original ShowWindow() call, you can try out the second one and the Sleep() afterwards.

Next I'm going to introduce the MSG structure. It is defined in the MSDN documentation as follows:

PHP Code

<?php 
typedef struct tagMSG 
{
  
HWND   hwnd;
  
UINT   message;
  
WPARAM wParam;
  
LPARAM lParam;
  
DWORD  time;
  
POINT  pt;
} 
MSG, *PMSG, *LPMSG; 

hwnd is the handle to the window that has a windows procedure that receives messages (you will learn more about this later), message is the message identifier, wParam and lParam contains additional information about this message, time is the time the message was sent, and pt is the posiiton of the cursor at the time of the message being sent.

Now it's time to populate our MSG structure, add the following:

PHP Code

<?php 
MSG msg 
= { 0 };

while(
msg.message != WM_QUIT) {
  if(
PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
    
TranslateMessage(&msg);
    
DispatchMessage(&msg);
  }
} 

We first create an instance of the data structure, called msg and we set all of its member variables to 0. We have created a while loop that continues as long as msg.message is not WM_QUIT. WM_QUIT is the message used to quit a program. For each iteration of the loop we check to see if PeekMessage returns true. If it returns NULL there are no messages in the "message queue" (the queue where message are kept until they can be processed). PeekMessage() takes arguments of the address of our MSG structure, the handle to a window where messages should be received, the first and last in the range of messages to be examined, and a flag specifiying how messages are handled. In this case it is PM_REMOVE (Removed from queue after being processed by PeekMessage). Other flags that could be used (may be read about in more detail here) instead are:
  • PM_NOREMOVE: Not removed from queue
  • PM_REMOVE: Removed from queue
  • PM_NOYIELD: Prevents threads being released that are waiting for the caller to go idle

All messages are automatically processed, but if you only want one or more types of messages to be processed you can also use the following flags:
  • PM_QS_INPUT: Mouse and keyboard messages
  • PM_QS_PAINT: Paint messages
  • PM_QS_POSTMESSAGE: All posted messages
  • PM_QS_SENDMESSAGE: All sent messages

These flags (like the window style flags) are separated by the bitwise OR operator (|).

TranslateMessage() will translate messages into character messages which are put into the message queue. DispatchMessage() will take messages from the queue and sends it to the window procedure, which is the next function we are going to create. Just to recap, your current wWinMain function should be as follows:

PHP Code

<?php 
int WINAPI wWinMain
(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow) {
  
UNREFERENCED_PARAMETER(prevInstance);
  
UNREFERENCED_PARAMETER(cmdLine);

  
WNDCLASSEX wndClass = { 0 };

  
wndClass.cbSize = sizeof(WNDCLASSEX);
  
wndClass.style = CS_HREDRAW | CS_VREDRAW;
  
wndClass.lpfnWndProc = WndProc;
  
wndClass.hInstance = hInstance;
  
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  
wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
  
wndClass.lpszMenuName = NULL;
  
wndClass.lpszClassName = "MyTutorialWindow";

  if(!
RegisterClassEx(&wndClass)) {
    return -
1;
  }

  
HWND hwnd = CreateWindow(
    
"MyTutorialWindow",
    
"Hello, World!",
    
WS_OVERLAPPEDWINDOW,
    
CW_USEDEFAULT,
    
CW_USEDEFAULT,
    
500,
    
300,
    
NULL,
    
NULL,
    
hInstance,
    
NULL
  
);

  if(!
hwnd) {
    return -
1;
  }

  
ShowWindow(hwnd, cmdShow);

  
MSG msg = { 0 };
  while(
msg.message != WM_QUIT) {
    if(
PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
      
TranslateMessage(&msg);
      
DispatchMessage(&msg);
    }
  }

  return 
static_cast<int>(msg.wParam);
} 





Now onto our windows procedure function. First, we need to write the prototype for our function, before the wWinMain function definition and after the #include directive write the following:

PHP Code

<?php 
LRESULT CALLBACK WndProc
(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); 

What this does is create the prototype for our WndProc (Windows Procedure). It takes 4 arguments; the handle to the window, the message identifier being sent, the first message parameter, and the second message parameter.

Now below the wWinMain function definition, create our WndProc function:

PHP Code

<?php 
LRESULT CALLBACK WndProc
(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
  return 
0;
} 

Now it's time to fill our WndProc function. First, we create a PAINTSTRUCT data structure that will hold information used to paint on the client window. After this we create a handle to the device context which can either be of type display, printer, memory, or information. The device context is used to support drawing or data retrieval on different types of devices.

PHP Code

<?php 
PAINTSTRUCT paintStruct
;
HDC hDC; 

Now we have to create a switch which uses cases defined by the message identifiers:

PHP Code

<?php 
switch(message) {
case 
WM_PAINT:
  break;

case 
WM_DESTROY:
  break;

default:
  break;
} 

This switch takes the message identifier passed to the WndProc function and does different things depending on this message identifier. In our switch we have included the WM_PAINT identifier and the WM_DESTROY identifier, but we have also included a default case in the event of being passed a message identifier that doesn't have a specific set of instructions attributed to it.

When populating this switch, each case should be in a block enclosed by braces, this prevents some errors which are caused when braces are left out (I'm not sure why though). For example:

PHP Code

<?php 
switch(variable) {
case 
VALUE:
  {
  
MyStatements...
  }
  break;
} 

So first, in the WM_PAINT case, add the following:

PHP Code

<?php 
{
hDC = BeginPaint(hwnd, &paintStruct);
EndPaint(hwnd, &paintStruct);
} 

BeginPaint takes a handle to the window being repainted and the address of our paintStruct structure. It uses these values and returns a device context handle but if it fails it will return NULL. EndPaint takes the handle to the window that has been repainted, and the address of paintStruct which should hold data passed to it from BeginPaint.

Now add the following to the WM_DESTROY case:

PHP Code

<?php 
{
PostQuitMessage(0);
} 

When the WM_DESTROY message identifer is sent, it will call PostQuitMessage. PostQuitMessage will take an argument that specifies the exit code (0 tends to mean everything executed correctly). The exit code is used as the wParam parameter of the WM_QUIT message. Because WM_QUIT is not associated with a window (it is a call for the entire application to quit) it does not need a case to be created in the window procedure. It is instead generated when PostQuitMessage is called.

Now add this to the default case:

PHP Code

<?php 
return DefWindowProc(hwnd, message, wParam, lParam); 

DefWindowProc takes 4 arguments, the handle to the window procedure that received the message, the message identifier, and two parameters for additional message information. It is used to handle any messages that haven't been handled by our switch cases in the WndProc function.

If you haven't caught on yet, the basic flow for a Windows program will be as follows:

  1. Program starts and your WinMain/wWinMain function is called
  2. Create your window class
  3. Register this class
  4. Create a window
  5. Initiate the message loop
  6. Check if the message queue contains anything
  7. If it does, call the windows procedure while passing the handle to whatever window is using this callback procedure
  8. Use the information in this message to decide what to do
  9. Act on this message
  10. Continue looping until WM_QUIT is posted as a message
  11. Quit the application

And now your window is complete! This is a basic window that will say "Hello, World!" in the title bar. The entire source code can be seen in the spoiler below:

Spoiler (Click to View)

(Jun 10, 2013 11:51 AM)Shihanus Wrote:  Another user completely mistaking the idea of the WIN Api.
Why is it that you idiots think just because it's the WIN Api, you're going to make GUIs?
That's not the WIN Api is for!
It's allows for the user to interact with the OS, simply that. I've been using the WIN Api for a couple of years now, and never created a GUI. Does that mean I'm doing something wrong?

I'm using it for forms creation in this thread and tutorial, if I was wanting to create a tutorial about networking or using the OS shell then I would have mentioned those reasons. As of now, I haven't looked at those uses of it so I'm not going to mention them. I'm sharing what I (think) I know so far. I like to try learn something and then post a tutorial and get feedback. I can then see if I've learned it correctly, and if I get constructive feedback about mistakes, I can fix it and I've learned something new.
You should have linked the MSDN documentation, so that people will know where to look at when they need explanation on certain function or parameter to pass to a function. Eventually, they will remember these and it'll be much easier.
(Jun 10, 2013 11:54 AM)Inso Wrote:  I'm using it for forms creation in this thread and tutorial, if I was wanting to create a tutorial about networking or using the OS shell then I would have mentioned those reasons. As of now, I haven't looked at those uses of it so I'm not going to mention them. I'm sharing what I (think) I know so far. I like to try learn something and then post a tutorial and get feedback. I can then see if I've learned it correctly, and if I get constructive feedback about mistakes, I can fix it and I've learned something new.

I didn't even necessarily mean networking or OS Shells.
If you wanted to give a tutorial on the WIN APi. You could have started with something even more basic.
Code
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
int main()
{
  MessageBoxA(GetDestkopWindow(),"Hello world!","MessageBox!",MB_OK);
  return 0;
}
There you have it, Win32 API with Hello World.
(Jun 10, 2013 11:58 AM)Shihanus Wrote:  I didn't even necessarily mean networking or OS Shells.
If you wanted to give a tutorial on the WIN APi. You could have started with something even more basic.
Code
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
int main()
{
  MessageBoxA(GetDestkopWindow(),"Hello world!","MessageBox!",MB_OK);
  return 0;
}
There you have it, Win32 API with Hello World.

I did think of that but I thought it wouldn't be worth a tutorial on just the message box. I was trying to learn off several sites and one of them was the MSDN docs, two of them mentioned the message box at the start but it only seemed to take up a small space for the overall tutorial.

I myself don't know too much about it (the entire WinAPI, just starting it basically), currently I'm just learning about adding controls but I don't know enough about it either which is why I left it out too. I'm a bit annoyed about that, one site I was looking at had some great tutorials that I was learning from but the next part of the series is being wrote and isn't up yet. :/

(Jun 10, 2013 11:58 AM)pr3fix Wrote:  You should have linked the MSDN documentation, so that people will know where to look at when they need explanation on certain function or parameter to pass to a function. Eventually, they will remember these and it'll be much easier.

I'll see if I can edit it in now, the thread was too big so it had to be done as two posts and once they merge it won't let you edit because of the character limit.

If not, the MSDN is referenced for a few of the functions and structures in there, so hopefully they'll be able to navigate from those links.

EDIT: Nope, sorry can't.

Quote:There was an error performing the update.

The message is too long. Please enter a message shorter than 18000 characters (currently 20733).
I stopped reading at
Code
Console based programs in C++ will use an actual main() function, but when programming with Win32 you use either WinMain() or wWinMain() as Windows.h will provide its own version of the main function.
(Jun 10, 2013 01:32 PM)Krofnica Wrote:  I stopped reading at
Code
Console based programs in C++ will use an actual main() function, but when programming with Win32 you use either WinMain() or wWinMain() as Windows.h will provide its own version of the main function.

I don't see what's wrong with that? It might not be as simple as I've wrote it, from what I've read it's the CRT that provides an implementation of main() but I thought it would just be simpler to write it as I did.
(Jun 10, 2013 01:41 PM)Inso Wrote:  I don't see what's wrong with that? It might not be as simple as I've wrote it, from what I've read it's the CRT that provides an implementation of main() but I thought it would just be simpler to write it as I did.

CRT defines a function which does some initialization before user-defined main function begins with execution. That function is usually called maincrtstartup, and is entry point of all c(++) programs. It does not only call main with prototypes:

int main();
int main(int, char**);

But it can also call:

int __stdcall WinMain(HINSTANCE, HINSTANCE, LPSTR, int);

windows.h does not define its own startup function :P Its all done by crt.
Oh right, guess I've learned something today. :P Thanks for the correction.
Thanks for this, I'm sure this will come in handy! Bookmarked.
Thank you very much :) I learned a lot and improved my C++ Skills.

And because I am a beginner at C++ I ported your code to C#. Pretty useless, but a nice challenge
Spoiler (Click to View)
Nice, thanks for the tutorial! I will reference it later!
Book. Marked.
35 Characters
Still 35 Characters
Apparently, very informative tutorial.. got bookmarked it now! Thanks a lot for this man!
Great guide. Was good to read. Thanks. 🤝
Don't forget to attack life with violence, speed, momentum, butt naked sprinting down the dark alleyways not afraid, DONT EVER STOOP!! - Dr. Disrespect.


Possibly Related Threads...
Thread / Author Replies Views Last Post
[C#] by notahackermanApr 26, 2020 11:32 PM 17 765 Apr 26, 2020 11:32 PM
Last Post: Shadow_Cloud
[Java] by The BrokerMar 22, 2020 05:46 PM 4 224 Mar 22, 2020 05:46 PM
Last Post: MavenGradle
[C] by SIM-3991524Mar 11, 2020 02:03 PM 11 341 Mar 11, 2020 02:03 PM
Last Post: some1