WTL for MFC Programmers, Part I - ATL GUI CLASSES

zhaozj2021-02-08  237

WTL for MFC Programmers, Part I - ATL GUI CLASSES

Original: Michael Dunn [English] Translation: Orbit (Star Rails Orbit) [http://www.winmsg.com/cn/orbit.htm]

Download Demonstration Code

This chapter

Readme.txt The overall introduction to this series of articles simple introduction to the first chapter ATL background knowledge

ATL and WTL Development History ATL-Style Template ATL Window Class Defines a window implementation

Fill in the Message Map Advanced Message Mapping Chain and Embedded (Mix-In Classes) ATL program structure ATL in the dialog, I will continue WTL, I promise! Modify record

Readme.txt

Before you start using the WTL or post the message in this article, I want to please read the following materials.

You need to develop a platform SDK (Platform SDK). You have to use WTL, you can't do it, you can use online upgrade to install the development platform SDK, or you can download all files locally after installing. Add the SDK's included file (.h header file) and library file (.lib file) path to the VC search directory, SDK has a ready-made tool to complete this work, this tool is located in the development platform SDK program group. " Visual Studio Registration "folder.

You need to install WTL. You can download the WTL version 7.0 from the Microsoft website. You can first view the "Introduction to WTL - Part 1" and "Easy Installation Of WTL" before installation, understand the information of the file to be installed, although now These articles have been out of time, but they can also provide a lot of useful information. There is a thing I think that it should not be mentioned in this article is to tell VC how to search for the WTL containing file path. If you use the VC6, click Tools / Options with the mouse, go to the Directories tab, list list in the display path Select Include Files in the box, then add the storage path of the WTL containing files to the list of included file search paths.

You need to know the MFC. Very good understanding of the MFC will help you understand the macros of the message map mentioned later and edit those code labeled "Do Not Edit" without problems.

You need to know how to use Win32 API programming. If you start learning Windows programming directly from MFC, you have not studied the API-level message processing method, that is very unfortunate that you will encounter trouble when using WTL. If you don't understand the meaning of WPARAM parameters and LPARAM parameters in the Windows message, you should understand that you need to read some articles (there is a lot of such articles in CodeProject).

You need to know the syntax of the C template, you can seek answers to VC Forum FAQ-related connections.

I just discussed some features that cover VC 6, but according to I learned that all programs can be used on VC 7. Since I don't use VC 7, I can't help those who have appeared in VC 7, but you can still feel your question here, because others may help you.

Overall introduction to this series of articles

WTL has two sides and it is true. It does not have a powerful interface (GUI) library, but it can generate a small executable. If you use MFC to interface like me, you will think that the interface control package provided by the MFC is very comfortable, not to mention the MFC's built-in message processing mechanism. Of course, if you are also like me, I don't want my own procedure just because of the size of the MFC, WTL is your choice. Of course, we also have to overcome some obstacles: ATL style template class seems to have a bit weirding without the support of class wizard, so you have to handle all message mappings. MSDN does not have formal document support, you need to collect the relevant documentation everywhere, or even view the source code of the WTL. If you can't buy a reference book, there is no Microsoft's official support ATL / WTL window with the MFC's window. The knowledge you know is not all applicable to WTL.

On the other hand, WTL also has its own advantages:

There is no need to learn or master complex document / view frames. The basic interface features of MFC, such as the DDX / DDV and command state automatic update (translator plus: CHECK tag, and enable tags for menus). Enhances some of the features of MFC (such as a more easy-to-use separator window). MFC programs that can be generated more than static links (translator plus: All source code for WTL is static link into your program). You can fix the wrong (bug) in the WTL you use without affecting other applications (below, if you fix the Bug's MFC / CRT dynamic library may cause other applications crash. If You still need to use the MFC, MFC window and the ATL / WTL window to "peaceful coexistence". (For example, a prototype in my work uses the MFC's cframewnd, and contains WTL CSPLitterWindow, in CSPLitterWindow It also uses the MFC cdialogs - I am not to show off, just modify the MFC code to use the WTL split window, which is much better than the MFC split window).

In this series of articles, I will first introduce ATL's window classes. After all, WTL is a series of additional classes on the ATL, so I need a good understanding of the ATL window class. After introducing the ATL, I will introduce the characteristics of the WTL and show how it makes interface programming easily.

Brief introduction to the first chapter

WTL is a cool tool, you need to introduce ATL before understanding this. WTL is a series of additional additional classes on the ATL. If you are a strict use of MFC programmers, you may have no chance to get in the interface class of ATL, so please tolerate some other things before starting WTL, WTL. Repairing the ATL is very necessary.

In the first part of this article, I will give a background knowledge of ATL, including some basic knowledge that must be known, quickly explains some ATL template classes and basic ATL window classes.

ATL background knowledge

Development history of ATL and WTL

"Active Template Library is a very quirky name, isn't it? Those older people may still remember that it is initially referred to as "Network Components Template", which may be its more accurate call, because the purpose of ATL is to make the writing component object and ActiveX control (ATL is developed in Microsoft New product ActiveX-a certain process is developed, those ActiveX-a certain .NET is now called a certain .NET). Since the ATL exists for easy writing component objects, only a simple interface class is provided, equivalent to the MFC window class (CDIALOG). Fortunately, these classes are very flexible and can construct additional classes such as WTL on the basis. WTL is now a second correction. The initial version is 3.1, and now the version is 7 (the reason whose WTL version number is selected to match the version of the ATL, so there is no 1 and 2 version number). WTL 3.1 can be used with VC 6 and VC 7, but several pre-processing labels need to be defined under VC 7. WTL 7 is compatible with WTL 3.1, and it can be used with VC 7 without any modification. It seems that there is no reason to use 3.1 to make new development work.

ATL-Style Template

Even if you can read the C template class code, there is still two things that may make you have some dizzy. The definition of this class is as an example:

Class CMYWND: PUBLIC CWINDOWIMPL {...};

This is legal because C syntax explains that even if the CMYWnd class is only partially defined, the class name CMYWND has been included in the recursive inheritance list, which is available. The parameters of the class name as the template are because the ATL wants another secret thing, that is, the virtual function call mechanism during compilation.

If you want to know how it works, please see the example below:

Template class b1 {public: void sayhi () {t * pt = static_cast (this); // huh? I will explain Pt-> PrintClassName ();} protected: void printclassname () {COUT << "this is b1";}}; Class D1: Public B1 {// no overridden functions at all}; Class D2: public B1 {protected: void printclassname () {cout << "this is d2";}}; main () {D1 D1; D2 D2; D1.SAYHI (); // prints "this is b1" d2. Sayhi (); //prints "this is d2"}

This code static_cast (this) is the trick. It will point to the B1 type pointer THIS assignment as a D1 or D2 type pointer, because the template code is generated by the template code, so as long as the compiler generates the correct inheritance list, this assignment is safe. (If you write:

Class D3: Public B1

There will be trouble) because the THIS object may only be the object to D1 or D2 (in some cases), it will not be other things. Note that this is very similar to the polymorphism, just the Sayhi () method is not a virtual function. To explain how this works, first look at the call to each Sayhi () function, in the first function call, the object B1 is assigned to D1, so the code is explained:

Void B1 :: Sayhi () {d1 * pt = static_cast (this); Pt-> PrintClassName ();

Since the D1 does not overload printClassName (), the base class B1, B1 has printClassName (), so the pRINTCLASSNAME () of B1 is called.

Now I look at the second function call Sayhi (), this time the object is assigned to the D2 type, Sayhi () is explained:

Void B1 :: Sayhi () {d2 * pt = static_cast (this); Pt-> PrintClassName ();

This time, D2 contains the PrintClassName () method, so the printClassName () method of D2 is called.

The advantage of this technology is:

No need to use a pointer to the object. Save memory because no virtual functions are required. Because there is no virtual function table, the virtual function pointing to the empty pointer points to the empty pointer will not occur. All function calls are determined at compile time (the translator plus: Dynamic compilation of the virtual function mechanism of C ), which is advantageous for the optimization of the compiler to the code.

Save the virtual function table seems to be in this example (only 4 bytes per virtual function), but imagine that if there is 15 base classes, each class contains 20 methods, add it. It is quite considerable.

ATL window class

Ok, about the background knowledge of ATL has been speaking, it is time to say ATL. ATL is designed to design the interface definition and implementation, which is the most obvious in the design of the window class. This is similar to the COM, COM interface definition and implementation is completely separated (or may have multiple implementations) .

ATL has an interface specifically designed for window, which can do all window operations, which is CWindow. It is actually a packaging class for HWND operations, encapsulated almost all window APIs with the HWND handle as the first parameter, such as: setWindowText () and DESTROYWINDOW (). The CWindow class has a public member M_HWnd, so that you can do it directly to the handle of the window, CWindow has an operator hWnd, you can talk about the CWindow object to the function of hwnd as a parameter, but this with cWnd :: getsafehwnd () Translator plus: MFC method) There is no equivalent.

CWindow's CWnd class is very different. Creating a CWindow object takes a lot of resources, because only one data member, there is no object chain in the MFC window, the object chain is maintained inside the MFC, this object chain will map HWND mapping Go to the CWND object. There is also a little different from the MFC's CWnd class is that when a CWindow object exceeds a scope, it is not destroyed, which means you don't need to remember the temporary CWindow object you created by you.

The implementation of the window process in the ATL class is CWINDOWIMPL. CWindowIMPL contains all windows implementation code, such as the registration of the window, the subclass of the window, the message mapping, and the basic WindowProc () function, you can see that this is very different from the MFC design, MFC will all code Placed in a CWND class. There are also two independent classes to include dialogs, which are CDIALOGIMPL and CAXDIALOGIMPL, CDIALOGIMPLs for implementing normal dialogs, and CaxDialogIMPL implements dialogs with ActiveX controls.

Define the implementation of a window

Any non-dialog window is derived from CWindowImpl, your new classes need to include three things:

One window class defines a message mapping chain window used by the default window type, called Window Traits

The definition of the window class is implemented via the Declare_Wnd_Class macro or Declare_Wnd_Class_ex macro. This macro defines a CWndClassInfo structure that encapsulates the WNDCLASSEX structure. DECLARE_WND_CLASS Macro makes you specify the class name of the window class, other parameters use the default setting, and the declare_wnd_class_ex macro also allows you to specify the type of window and the background color of the window, you can also use null as class name, ATL will automatically generate one Class name.

Let's start defining a new class, in the following chapters I will gradually complete the definition of this class.

Class CMYWINDOW: PUBLIC CWINDOWIMPL {public: Declare_Wnd_Class (_T ("My Window Class")};

Next is the message mapping chain, the ATL message mapping chain is more simple than the MFC, the ATL message mapping chain is expanded to the switch statement, the Switch statement correct message handler and calls the corresponding function. The macro using the message mapping chain is begin_msg_map and end_msg_map, let us add an empty message mapping chain for our window.

Class CMYWINDOW: PUBLIC CWINDOWIMPL {public: declare_wnd_class (_t ("my window class") begin_msg_map (cmywindow) end_msg_map ()};

I will show how to add messages to the message mapping chain in the next section. Finally, we need the feature of our window-defined window, the feature of the window is the federation of the window type and extended window type, which is used to create a window specified by the window. The window type is specified as a parameter template, so the caller of the window does not need to worry about the correct type of the specified window, and below is an example of the type of the ATL Class CWINTraits Define window type:

typedef CWinTraits CMyWindowTraits; class CMyWindow: public CWindowImpl {public: DECLARE_WND_CLASS (_T ( "My Window Class")) BEGIN_MSG_MAP (CMyWindow) END_MSG_MAP ()};

The caller can overload the type definition of CMyWindowTraits, but in general, this is not necessary. ATL provides several pre-defined special types, one of which is CFraMewintraits, a great frame window: typedef cwints CFRAMEWINTRAITS;

Fill in the message mapping chain

The ATL message mapping chain is a unfamiliar part of the developer and is also the largest part of the WTL to improve it. The class wizard allows you to add a message response, however ATL does not have a message-related macro and the parameter of the MFC automatically expands, only three types of messages in the ATL, one is WM_NOTIFY, one is WM_COMMAND, the third category is other Window messages let us start adding WM_CLOSE and WM_DESTROY messages to our window.

class CMyWindow: public CWindowImpl {public: DECLARE_WND_CLASS (_T ( "My Window Class")) BEGIN_MSG_MAP (CMyWindow) MESSAGE_HANDLER (WM_CLOSE, OnClose) MESSAGE_HANDLER (WM_DESTROY, OnDestroy) END_MSG_MAP () LRESULT OnClose (UINT uMsg , WPARAM WPARAM, BOOL & BHANDERD {DESTROYWINDOW (); Return 0;} LRESULT ONDESTROY (UINT UMSG, WPARAM WPARAM, LPARAM LPARAM, BOOL & BHAND) {PostquitMessage (0); Return 0;}};

You may notice that the message response function is that the original WPARAM and LPARAM values, you need to expand it to the parameters required for the corresponding message. There is also the fourth parameter bhandled, this parameter is set to TRUE in the message corresponding function call, if you need ATL call default WindowProc (), you can talk to false. This is different from the MFC, and the MFC is the default message processing of the response function that is displayed.

Let us also add a handle to the WM_Command message, assume that our window has an ID of IDC_about:

class CMyWindow: public CWindowImpl {public: DECLARE_WND_CLASS (_T ( "My Window Class")) BEGIN_MSG_MAP (CMyWindow) MESSAGE_HANDLER (WM_CLOSE, OnClose) MESSAGE_HANDLER (WM_DESTROY, OnDestroy) COMMAND_ID_HANDLER (IDC_ABOUT, OnAbout) END_MSG_MAP ( ) LRESULT OnClose (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) {DestroyWindow (); return 0;} LRESULT OnDestroy (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) {PostQuitMessage (0); return 0;} Lresult Onabout (Word Word HWnd Code, Bool & Bhand) {MessageBox (_T ("Sample ATL WINDOW"), _T ("About MyWindow"); return 0;}}; need to pay attention to the command_handler macro has already The parameters of the message are expanded. Similarly, the Notify_Handler Macro also expands the parameters of the WM_NOTIFY message.

Advanced message mapping chain and embedded class

Another significant difference between ATL is that any C class can respond to messages, while MFC simply distributes message response tasks to CWnd classes and ccmdtarget classes, plus classes with pretranslateMessage () methods. This feature of ATL allows us to write so-called "embedded classes", add features to our window, just add this class to inheritance list, it is as simple as it!

A basic page class is usually template class, which will derive class names as a parameter of the template so that it can access members in the derived class, such as m_hwnd (HWND members in the CWindow class). Let's take a look at an example of embedded classes, this embedded class draws the background of the window by responding to the WM_ERASEBKGND message.

template class CPaintBkgnd: public CMessageMap {public: CPaintBkgnd () {m_hbrBkgnd = CreateSolidBrush (t_crBrushColor);} ~ CPaintBkgnd () {DeleteObject (m_hbrBkgnd);} BEGIN_MSG_MAP (CPaintBkgnd) MESSAGE_HANDLER (WM_ERASEBKGND, OnEraseBkgnd) END_MSG_MAP () LRESULT OneRaseBkGnd (UINT UMSG, WPARAM WPARAM, LPARAM LPARAM, BOOL & BHAND) {T * Pt = Static_cast (this); HDC DC = (HDC) WPARAM; Rect RcClient; PT-> getClientRect (& rcclient); FillRect (DC, & RCCLIENT, M_HBRBKGND); RETURN 1; // We painted the background} protected: hbrush m_hbrbkgnd;}; let us study this new class. First, CPaintBkgn has two template parameters: the name of the derived class of cpaintbkgn and the color used to draw a window background. (T_ prefix is ​​usually used as a template parameter of the template class) CPAINTBKGND is also derived from cMessageMap, which is not necessary, because all classes that need to respond to messages are enough to use Begin_MSG_Map macro, so you may see other Some examples of embedded classes, they are not derived from the base class.

The constructor and the destructor are quite simple, just create and destroy the Windows brush, this painting brush determines the color by the parameter t_crbrushcolor. Next, a message mapping chain, which responds to the WM_ERASEBKGND message, and finally the background of the brush-filled window created by the response function onerasebkgnd (). There are two things in OneRaseBkGnd () to note that one is a method of derived window classes (ie getClientRect ()), how do we know that there is a getClientRect () method in the derived class? If this method does not have this method in the derive class, our code will not have any complaints, and confirmed by the compiler to derive from CWindow. The other is OneRaseBkGnd () does not expand the message parameter WPARAM to the device context (DC). (WTL will eventually solve this problem, we can see it soon, I promise)

To use this embedding in our window, you need to do two things: First, add it to the inheritance list:

Class CMyWindow: Public CWindowImpl , Public CpaintBkgn

Second, it is necessary to pass the message to cpaintBkgnd, which is to link it into the message mapping chain, add a chain_msg_map macro in the CMYWINDOW message mapping chain:

class CMyWindow: public CWindowImpl , public CPaintBkgnd {... typedef CPaintBkgnd CPaintBkgndBase; BEGIN_MSG_MAP (CMyWindow) MESSAGE_HANDLER (WM_CLOSE , OnClose) Message_Handler (WM_DESTROY, ONDESTROY) Command_Handler (idc_about, onabout) CHAIN_MSG_MAP (CPAINTBKGNDBASE) End_msg_map () ...}; Any CMYWINDOW has not been processed messages to cpaintbkgnd. It should be noted that WM_CLOSE, WM_DESTROY and IDC_ABOUT messages will not be passed because these messages will be aborted once they are processed message mapping chains. It is necessary to use TypedEf because macro is a pre-processing macro. It can only have a parameter. If we pass CPaintBkgn as parameters, that "," will make the preprocessor think we use Multiple parameters.

You can use multiple embed classes in the inheritance list, each embedded class uses a chain_msg_map macro so that the message mapping chain will pass the message to it. This is different from MFC, and the MFC CWnd derived class can only have one base class, and the MFC automatically passes the message to the base class.

ATL program structure

We have already had a complete landlord window class (even if it is not fully useful), let us see how to use it in the program. An ATL program contains a ccommodule type global variable _Module, this and the MFC program has a CWINAPP type global variable theApp Some similar, the only difference is that this variable in ATL must be named _module.

Below is the beginning of the stdafx.h file:

// stdafx.h: #define strict #define vc_extralean #include // Basic ATL EXTERN CCOMMODULE _MODULE; / / Global _Module #include // ATL window class

Atlbase.h already contains the most basic Window programming header file, so we don't need to include header files that contain Windows.h, Tchar.h. Declare _module variables in the CPP file:

// main.cpp: ccommodule _module;

CCOMModule contains the initialization and closing function of the program, and needs to be displayed in WinMain (), let us start here:

// main.cpp: ccommodule _Module; int WinStance Hinst, Hinstance Hinstprev, LPSTR SZCMDLINE, INT NCMDSHOW) {_Module.init (null, hinst); _Module.Term ();}

The first parameter of init () is only useful if the COM service program is useful. Since our EXE does not contain a COM object, we only need to pass null to init (). ATL does not provide your own WinMain () and a MFC messaging pump, so we need to create a CMYWINDOW object and add a message pump to make our program run. // main.cpp: #include "MyWindow.h" CComModule _Module; int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) {_Module.Init (NULL, hInst); CMyWindow wndMain; MSG msg; // Create & Show Our Main WINDOW IF (NULL == Wndmain.create (Null, CWindow :: Rcdefault, _T ("My First ATL WINDOW")) {// Bad News, Window Creation Failed Return 1;} WNDMAIN.SHOWINDOW Ncmdshow; wndmain.UpdateWindow (); // run the message loop while (GetMessage (& MSG, NULL, 0, 0)> 0) {TranslateMessage (& MSG); DispatchMessage (& MSG);} _module.term (); return msg .wpaham;

The only thing on the code needs to be described in CWindow :: RcDefault, which is a member (static data member) in CWindow, and the data type is RECT. When calling the CREATEWINDOW () API, use the CW_USEDEFAULT to specify the width and height of the window, ATL uses RCDefault as the initial size of the window.

Inside the ATL code, the ATL uses some similar assembly language magic to connect the handle of the main window with the corresponding CMYWINDOW object. It seems that the CWindow object can be passed between threads without problems, while the MFC CWnd is Can't do this.

This is our window:

I have to admit that this doesn't have any exciting place. We will add an About menu and display a dialog, mainly to add some fun.

Dialog in ATL

We mentioned earlier, ATL has two dialog boxes, and our About dialog uses CDialogImpl. Generating a new dialog and generating a main window almost two points:

The base class of the window is CDIALOGIMPL instead of CWINDOWIMPL. You need to define a public member named IDD to save the ID of the dialog resource.

Now start to define a new class for the About dialog:

Class Caboutdlg: Public CDialogIMPL {public: enum {IDD = IDD_ABOUT}; begin_msg_map (caboutdlg) end_msg_map ()};

ATL does not implement the response processing of "OK" and "Cancel" on the internal implementation, so we need to add these code yourself. If the user clicks on the closing button of the title bar, the WM_CLOSE's response function will be called. We also need to handle the WM_INITDIALOG message so that we can set the keyboard focus when the dialog box appears, and below is a complete class definition and message response function. class CAboutDlg: public CDialogImpl {public: enum {IDD = IDD_ABOUT}; BEGIN_MSG_MAP (CAboutDlg) MESSAGE_HANDLER (WM_INITDIALOG, OnInitDialog) MESSAGE_HANDLER (WM_CLOSE, OnClose) COMMAND_ID_HANDLER (IDOK, OnOKCancel) COMMAND_ID_HANDLER (IDCANCEL, OnOKCancel) END_MSG_MAP () LRESULT OnInitDialog (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) {CenterWindow (); return TRUE; // let the system set the focus} LRESULT OnClose (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) {EndDialog (IDCANCEL ); Return 0;} Lresult Onokcancel (Word WRESTLYCODE, WORD, HWND HWNDCTL, BOOL & BHANDED) {enddialog (wid); Return 0;}}

I use a message response function to handle the WM_COMMAND message of "OK" and "Cancel", because the WID parameters of the command response function have indicated that the message is from the "OK" buttons from the "Cancel" button.

The method of displaying the dialog is similar to the MFC, create an instance of a new dialog class, and then call the Domodal () method. Now let's return to the main window, add a menu with the About menu item to display our dialog, this needs to add two message response functions, one is responding to WM_CREATE, the other is the idc_about command of the response menu.

class CMyWindow: public CWindowImpl , public CPaintBkgnd {public: BEGIN_MSG_MAP (CMyWindow) MESSAGE_HANDLER (WM_CREATE, OnCreate) COMMAND_ID_HANDLER (IDC_ABOUT, OnAbout) // ... CHAIN_MSG_MAP (CPaintBkgndBase) END_MSG_MAP () LRESULT OnCreate (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) {HMENU hmenu = LoadMenu (_Module.GetResourceInstance (), MAKEINTRESOURCE (IDR_MENU1)); SetMenu (hmenu); return 0;} LRESULT OnAbout (Word WRIFYCODE, WORD WID, HWND HWNDCTL, BOOL & BHAND) {Caboutdlg DLG; DLG.Domodal (); Return 0;} // ...}; somewhat different in the form of the parent window of the specified dialog, MFC is passed The constructor passes the parent window's pointer to the dialog and is passed to the first parameter of the parent window as the first parameter of the Domodal () method. If the code above is not specified, the parent window is specified, ATL will Use the getActiveWindow () to get the window (that is, our main frame window) as the parent window of the dialog.

The call to the loadMenu () method shows another method of CCommodule -GetResourceInstance (), which returns your EXE's Hinstance instance, and the MFC's AFXGETRESOURCEHANDLE () method is similar. (Of course there is CCommodule :: getModuleInstance (), it is equivalent to the MFC's AFXGetInstanceHandle ().)

This is the display effect of the main window and dialog:

I will continue to tell WTL, I promise!

I will continue to talk about WTL, just in the second part. I think that since these articles are written by using the MFC, it is necessary to introduce some ATL before investing in the WTL. If you are in contact with ATL for the first time, now you can try to write some small programs, process messages, and use embedded classes, you can also try to support message mapping chains with class wizards to automatically add a message response. Start now, right-click the CMYWINDOW item, click the "Add Windows Message Handler" menu item in the pop-up context menu.

In the second part, I will fully introduce basic WTL window classes and a better message mapping macro.

Modify record

On March 22, 2003, this article was first published.

转载请注明原文地址:https://www.9cbs.com/read-2283.html

New Post(0)