分享
 
 
 

Kylix预览

王朝delphi·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

Cross-platform Controls

From Windows to linux, and Back

These are exciting times for Borland. Not since the first whisper of Delphi has there been this much excitement about a Borland PRoduct. I'm talking, of course, about Kylix, the project to bring C++Builder and Delphi to the Linux Operating system. The Delphi version will be available first, so for the rest of this article, Kylix refers to Delphi for Linux.

We're developing a new VCL that will work with the Windows and Linux versions of Delphi. This means you can write an application in Windows, then move the source to a Linux box and recompile it - or vice versa. This new VCL is named CLX, for Component Library Cross-Platform. CLX encompasses the entire cross-platform library distributed with Kylix. There are a few sub-categories, which, as of this writing, break down as follows:

BaseCLX is the RTL, up to, and including, Classes.pas.

VisualCLX includes the user interface classes, i.e. the usual controls.

DataCLX comprises the cross-platform database components.

NetCLX includes the Internet stuff, e.g. Apache, etc.

At the time of this writing [early May 2000], the first Field Test for Kylix is just beginning. By the time you read this, there will be a big difference between the Kylix I'm using and working on, and the version you'll see when it's available. This makes my job all that more difficult. It would be easy to talk in generalities, waxing eloquent about the underlying architecture. I'd much rather discuss the details, however, so you can get a head start producing CLX controls. Just keep in mind that it's likely some of the particulars discussed in this article will have changed by the time you read it.

No One Else Comes Close

This article is a primer on writing custom VisualCLX controls. Essentially, the VisualCLX is what you know and love about the VCL. When you think about it, Visual Component Library is a bit of a misnomer; there's a lot more to it than the visual components. In this article, however, I'm only going to write about the visual controls. The Button, Edit, ListBox, PageControl, StatusBar, ProgressBar, etc. controls, have all been re-implemented to be cross-platform. How did we do this when the current VCL relies so much on Windows? In brief, we ripped out all the Windows stuff, and replaced it with another toolkit.

In Linux, there are a number of toolkits that contain the standard windowing controls, such as Buttons. They're called widgets, and GTK and Qt (pronounced "cute") are two of the more popular. Qt is a Linux widget toolkit that works on Windows and Linux. Because it aligned most closely with our goals, Qt was chosen as the basis for CLX. In other Words, Qt is to CLX what the Windows API and common controls are to the VCL. Qt has some definite positives for the Delphi custom component developer on Linux:

It's a prevalent Linux widget set, used by the popular KDE desktop.

It's similar to the Windows API style of development.

Its graphics model is close to the VCL's graphics model.

Its classes look very much like VCL components.

It introduces many standard widgets, and handles the message loop.

This begs two questions: Does this mean that Kylix supports only KDE, and no other desktops, such as Gnome? And how does using Qt as the basis of CLX affect me? The answer to the first question is that Kylix applications will run under any Linux desktop - particularly Gnome and KDE. The rest of this article answers the second question.

Don't Want You Back

The goal is to make it easy for developers to port their applications to Linux with the least amount of trouble. Most of the component names are the same, and most of the properties are the same. Although a few properties will be missing from some components, and a few new ones will be added, for the most part, it should be fairly painless to port your applications.

For component writers, it's a different story. For starters, there is no Windows.pas, nor Windows API (see Figure 1). You can say good-bye to the message directive, and all of the CN and CM notifications. These have been changed to dynamics. There is also no docking, bi-directional (BiDi) methods/properties, input method editor (IME), or Asian support in the first release. Of course, there's no ActiveX, COM, or OLE support. Windows 3.1 components are also out, as I write this.

Methods

CreateParams

CreateSubClass

CreateWindowHandle

CreateWnd

DestroyWindowHandle

DestroyWnd

DoAddDockClient

DockOver

DoDockOver

DoRemoveDockClient

DoUnDock

GetDeviceContext

MainWndProc

ResetIme

ResetImeComposition

SetIme

SetImeCompositionWindow

WndProc

Properties

Ctl3D

DefWndProc

DockManager

DockSite

ImeMode

ImeName

ParentCtl3D

UseDockManager

WheelAccumulator

Figure 1: Methods and properties missing from TWidgetControl (formerly known as TWinControl).

By now I bet you're thinking, "That's not so bad; porting my components doesn't sound too difficult." But wait - there's more. At the time of this writing, the CLX unit names have all been changed to include "Q" as a prefix. So StdCtrls is now QStdCtrls, some classes have shuffled around a bit, and there are some subtle differences in the hierarchy (see Figure 2).

Figure 2: The differences in the class hierarchy are subtle.

The CLX prefix of "Q" may or may not end up as the permanent prefix in the final release. TWinControl is now a TWidgetControl, but to ease the pain we added a TWinControl alias to TWidgetControl. TWidgetControl and descendants all have a Handle property that is an opaque reference to the Qt object; and a Hooks property, which is a reference to the hook objects that handle the event mechanism. (Hooks are part of a complex topic that is outside the scope of this article.)

OwnerDraw will be replaced with a new idea called Styles. Styles are basically a mechanism whereby a widget or application can take on a whole new look, similar to skins in Windows. This is something that's still in development, so I'm not going to discuss it further in this article. I will say this: It's way cool.

Is anything the same? Sure. TCanvas is the same as you remember, with its collection of Pens and

Brushes. As I mentioned, the class hierarchy is basically the same, and events, such as OnMouseDown, OnMouseMove, OnClick, etc., are still there.

Show Me the Meaning ...

Let's move on to the meat of CLX, and see how it works. Qt is a C++ toolkit, so all of its widgets are C++ objects. On the other hand, CLX is written in Object Pascal, and Object Pascal can't talk directly to C++ objects. To make matters a little more difficult, Qt uses multiple inheritance in a few places. So we created an interface layer that takes all of the Qt classes and reduces them to a series of straight C functions. These are then wrapped up in a DLL in Windows and a shared object in Linux.

Every TWidgetControl has CreateWidget, InitWidget, and HookEvents virtual methods that almost always have to be overridden. CreateWidget creates the Qt widget, and assigns the Handle to the FHandle private field variable. InitWidget gets called after the widget is constructed, and the Handle is valid. Some of your property assignments will move from the Create constructor to InitWidget. This will allow delayed construction of the Qt object until it's really needed. For example, say you have a property named Color. In SetColor, you can check with HandleAllocated to see if you have a Qt handle. If the Handle is allocated, you can make the proper call to Qt to set the color. If not, you can store the value in a private field variable, and, in InitWidget, you set the property.

There are two types of events: Widget and System. HookEvents is a virtual method that hooks the CLX controls event methods to a special hook object that communicates with the Qt object. (At least that's how I like to look at it.) The hook object is really just a set of method pointers. System events now go through EventHandler, which is basically a replacement for WndProc.

Larger Than Life

All of this is just background information, because you really don't need to know it in order to write cross-platform custom controls. It helps, but with CLX, writing cross-platform controls is a snap. Just as you didn't have to understand the complexities of the Windows API to write a VCL control, the same goes for CLX and Qt. Listing One shows a custom control written with CLX. [It's available for download; see end of article for details.]

The project file, CalcTest.dpr, is shown in Figure 3. The calculator control running in Windows (shown in Figure 4), and in Linux (shown in Figure 5) looks much like the standard Microsoft Windows calculator.

program CalcTest;

uses

SysUtils, Classes, QControls, QForms, QStdCtrls, Qt,

QComCtrls, QCalc, Types;

type

TTestForm = class(TForm)

Calc: TCalculator;

public

constructor Create(AOwner: TComponent); override;

end;

var

TestForm: TTestForm;

{ TTestForm }

constructor TTestForm.Create(AOwner: TComponent);

begin

inherited CreateNew(AOwner);

SetBounds(10,100,640,480);

Calc := TCalculator.Create(Self);

// Don't forget: we have to set the parent.

Calc.Parent := Self;

Calc.Top := 100;

Calc.Left := 200;

// Uncomment these to try other Border effects:

// Calc.BorderStyle := bsEtched;

end;

begin

Application := TApplication.Create(nil);

Application.CreateForm(TTestForm, TestForm);

TestForm.Show;

Application.Run;

end.

Figure 3: The project file for the CLX calculator control.

Figure 4: The calculator in Windows at run time.

Figure 5: The control at run time, as it appears on Red Hat Linux.

As you can see, TCalculator is a descendant of TFrameControl. TFrameControl is a new control introduced to the hierarchy under TWidgetControl that provides a frame for your controls. The property we're most interested in is BorderStyle:

TBorderStyle = (bsNone, bsSingle, bsDouble, bsRaisedPanel,bsSunkenPanel,

bsRaised3d, bsSUnken3d, bs Etched, bsEmbossed);

There are two important methods in this control. BuildCalc creates all of the buttons, and places them in their proper locations. As you can see, I used an enumerator named TButtonType to hold the "function" of the button, and this tidbit of information is stored as an integer in the Tag property. I refer to this later in the Calc method. All of the calculator buttons are stored in a protected array of TButtonRecord records named Btns:

TButtonRecord = record

Top: Integer;

Left: Integer;

Width: Integer;

Height: Integer;

Caption: string;

Color: TColor;

end;

This makes it easy to set up all of the buttons in a loop, rather than using an ugly bunch of TButton.Create calls. Notice that the buttons' OnClick handlers get assigned to the TCalculator's Calc method. It's alright to do a direct assignment to what is typically a user event, because all of these buttons are internal to the calculator, and these events won't be published (see Figure 6).

for i := Low(TButtonType) to High(TButtonType) do

with TButton.Create(Self) do begin

Parent := Self;

SetBounds(Btns[i].Left, Btns[i].Top, Btns[i].Width,

Btns[i].Height);

Caption := Btns[i].Caption;

Color := Btns[i].Color;

OnClick := Calc;

Tag := Ord(i);

end;

Figure 6: A direct assignment to a user event is okay in this case.

I have a TLabel called FStatus. TLabel also descends from TFrameControl. I wanted it in the calculator, so I could get the "sunken box" look for the memory display like the Windows calculator. The Qt label widget is really a lot like the VCL TPanel component. For the TLabel in CLX, we don't publish the frame properties, but that doesn't stop you from using them in your descendants.

The last thing I do in BuildCalc is to create the edit control to display the results of the calculation. As you can see, the Text property of the calculator hooks directly to the Text property of the Edit control.

The other main method is Calc, which is essentially a huge case statement that evaluates which button was pushed, and decides what to do about it. I use the private field variables FCurrentValue, FLastValue, and FRepeatValue to handle the value of the calculations, so I don't have to implement a stack. The idea was to show how to create a cross-platform control, not how to write a calculator.

Oh yeah! Remember, that I used the Tag property in BuildCalc to hold its function? That's retrieved in this method by casting the Sender to a TButton, and casting the Tag back to a TButtonType. ButtonType is the selector expression of the case statement:

ButtonType := TButtonType(TButton(Sender).Tag);

Are you wondering how we convert this to a cross-platform control? No? Good! That means you've been paying attention. This code will compile in Windows and Linux with absolutely no changes. There are no extra steps involved. Just by the virtue of using CLX, this control is ready to go.

All I Have to Give

As you can see, writing a cross-platform control isn't all that different from writing a VCL component. If you're a new component developer, it won't be difficult to learn. If you're an experienced VCL component builder, most of your knowledge will transfer to Kylix nicely.

As I said earlier, there are a lot of differences, but that should only affect developers who have components that rely on the Windows API. If you wrote a control that was a descendant of a VCL control, an aggregate of a few controls (as I did here with TCalculator), a non-visual component that doesn't rely on the Windows API, or was a TGraphic control, then you shouldn't have much trouble porting it to Linux.

This article describes features of software products that are in development and subject to change without notice. Description of such features here is speculative and does not constitute a binding contract or commitment of service.

The files referenced in this article are available for download.

Involved as a user of Delphi since the initial beta, Robert Kozak is a member of the Kylix R&D team and has been with Borland since the later half of 1999. Since he joined Borland, he has been involved in the development of C++Builder 5 and Kylix. Robert was involved with the start of TaDDA! (Toronto Area Delphi Developers Association), which later merged with TDUG (Toronto Delphi Users Group). Robert continues to stay active in the user community, and is active on the Borland newsgroups.

Begin Listing One - QCalc.pas

{ ***************************************************** }

{ }

{ Borland Delphi Visual Component Library }

{ Borland Delphi Component Library (X)Crossplatform }

{ }

{ Copyright (c) 2000 Inprise/Borland }

{ }

{ ***************************************************** }

unit QCalc;

// This is the very first Custom control written for CLX.

interface

uses

Sysutils, Classes, QT, QControls, QStdCtrls, QComCtrls,

QGraphics;

type

TButtonType = (bt0, bt1, bt2, bt3, bt4, bt5, bt6, bt7,

bt8, bt9, btDecimal, btPlusMinus, btMultiply, btDivide,

btAdd, btSubtract, btSqrt, btPercent, btInverse,

btEquals, btBackspace, btClear, btClearAll,

btMemoryRecall, btMemoryStore, btMemoryClear,

btMemoryAdd);

TCalcState = (csNone, csAdd, CSSubtract, csMultiply,

csDivide);

TButtonRecord = record

Top: Integer;

Left: Integer;

Width: Integer;

Height: Integer;

Caption: string;

Color: TColor;

end;

TCalculator = class(TFrameControl)

private

FResultEdit: TEdit;

FStatus: TLabel;

FMemoryValue: Single;

FCurrentValue: Single;

FLastValue: Single;

FRepeatValue: Single;

FState: TCalcState;

FBackSpaceValid: Boolean;

protected

Btns: array [TButtonType] of TButtonRecord;

procedure BuildCalc;

procedure Calc(Sender: TObject);

function GetText : string; override;

procedure SetText(const Value : string); override;

public

constructor Create(AOwner: TComponent); override;

property Value : Single read FCurrentValue;

published

property Text : string read GetText write SetText;

property BorderStyle;

property LineWidth;

property Margin;

property MidLineWidth;

property FrameRect;

end;

implementation

function ButtonRecord(aTop, aLeft, aWidth,

aHeight: Integer; aCaption: string;

aColor: TColor = clBtnFace): TButtonRecord;

begin

Result.Top := aTop;

Result.Left := aLeft;

Result.Width := aWidth;

Result.Height := aHeight;

Result.Caption := aCaption;

Result.Color := aColor;

end;

{ TCalculator }

constructor TCalculator.Create(AOwner: TComponent);

begin

inherited Create(AOwner);

SetBounds(0,0,250,200);

FMemoryValue := 0;

FCurrentValue := 0;

FLastValue := 0;

FRepeatValue := 0;

BorderStyle := bsRaisedPanel;

BuildCalc;

end;

procedure TCalculator.BuildCalc;

var

i: TButtonType;

begin

Btns[bt7] := ButtonRecord(70, 48, 36, 29, '7');

Btns[bt4] := ButtonRecord(102, 48, 36, 29, '4');

Btns[bt1] := ButtonRecord(134, 48, 36, 29, '1');

Btns[bt0] := ButtonRecord(166, 48, 36, 29, '0');

Btns[bt8] := ButtonRecord(70, 88, 36, 29, '8');

Btns[bt5] := ButtonRecord(102, 88, 36, 29, '5');

Btns[bt2] := ButtonRecord(134, 88, 36, 29, '2');

Btns[btPlusMinus] :=

ButtonRecord(166, 88, 36, 29, '+/-');

Btns[bt9] := ButtonRecord(70, 128, 36, 29, '9');

Btns[bt6] := ButtonRecord(102, 128, 36, 29, '6');

Btns[bt3] := ButtonRecord(134, 128, 36, 29, '3');

Btns[btDecimal] := ButtonRecord(166, 128, 36, 29, '.');

Btns[btDivide] := ButtonRecord(70, 168, 36, 29, '/');

Btns[btMultiply] := ButtonRecord(102, 168, 36, 29, '*');

Btns[btSubtract] := ButtonRecord(134, 168, 36, 29, '-');

Btns[btAdd] := ButtonRecord(166, 168, 36, 29, '+');

Btns[btBackspace] :=

ButtonRecord(37, 49, 63, 25, 'Backspace');

Btns[btClear] := ButtonRecord(37, 115, 63, 25, 'CE');

Btns[btClearAll] := ButtonRecord(37, 181, 63, 25, 'C');

Btns[btsqrt] := ButtonRecord(70, 208, 36, 29, 'sqrt');

Btns[btPercent] := ButtonRecord(102, 208, 36, 29, '%');

Btns[btInverse] := ButtonRecord(134, 208, 36, 29, '1/x');

Btns[btEquals] := ButtonRecord(166, 208, 36, 29, '=');

Btns[btMemoryAdd] := ButtonRecord(166, 5, 36, 29, 'M+');

Btns[btMemoryStore] :=

ButtonRecord(134, 5, 36, 29, 'MS');

Btns[btMemoryRecall] :=

ButtonRecord(102, 5, 36, 29, 'MR');

Btns[btMemoryClear] := ButtonRecord(70, 5, 36, 29, 'MC');

for i := Low(TButtonType) to High(TButtonType) do

with TButton.Create(Self) do begin

Parent := Self;

SetBounds(Btns[i].Left, Btns[i].Top, Btns[i].Width,

Btns[i].Height);

Caption := Btns[i].Caption;

Color := Btns[i].Color;

OnClick := Calc;

Tag := Ord(i);

end;

FStatus := TLabel.Create(Self);

with FStatus do begin

Parent := Self;

SetBounds(10, 38, 25, 25);

BorderStyle := bsRaisedPanel;

end;

FResultEdit := TEdit.Create(Self);

FResultEdit.Parent := Self;

FResultEdit.SetBounds(5, 5, 240, 25);

FResultEdit.Alignment := taRightJustify;

FResultEdit.Font.Height := -13;

FResultEdit.Font.Name := 'Arial';

FResultEdit.Text := '0';

end;

procedure TCalculator.Calc(Sender: TObject);

const

MemoryStoreMap: array [Boolean] of string = (' M',');

var

ButtonType: TButtonType;

Temp: string;

TempValue: Single;

begin

ButtonType := TButtonType(TButton(Sender).Tag);

try

case ButtonType of

bt0..bt9:

begin

FBackSpaceValid := True;

if (FResultEdit.Text = '0') or

(FCurrentValue = 0) then

FResultEdit.Text := ';

FResultEdit.Text :=

FResultEdit.Text + Btns[ButtonType].Caption;

FCurrentValue := StrToFloat(FResultEdit.Text);

FRepeatValue := 0;

end;

btDecimal:

if Pos('.', FResultEdit.Text) < 1 then begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FLastValue := 0;

FResultEdit.Text :=

FResultEdit.Text + Btns[ButtonType].Caption;

end;

btPlusMinus:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FCurrentValue := FCurrentValue * -1;

FResultEdit.Text := FloatToStr(FCurrentValue);

end;

btClearAll:

begin

FCurrentValue := 0;

FLastValue := 0;

FResultEdit.Text := '0';

FState := csNone;

end;

btClear:

begin

FCurrentValue := 0;

FResultEdit.Text := '0';

end;

btAdd:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FState := csAdd;

FLastValue := FCurrentValue;

FCurrentValue := 0;

end;

btSubtract:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FState := csSubtract;

FLastValue := FCurrentValue;

FCurrentValue := 0;

end;

btDivide:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FState := csDivide;

FLastValue := FCurrentValue;

FCurrentValue := 0;

end;

btMultiply:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FState := csMultiply;

FLastValue := FCurrentValue;

FCurrentValue := 0;

end;

btBackSpace:

if FBackSpaceValid then begin

Temp := FResultEdit.Text;

Delete(Temp, Length(Temp),1);

if Temp = ' then

Temp := '0';

FCurrentValue := StrToFloat(Temp);

FResultEdit.Text := FloatToStr(FCurrentValue);

end;

btInverse:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FCurrentValue := 1 / FCurrentValue;

FResultEdit.Text := FloatToStr(FCurrentValue);

end;

btPercent:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FCurrentValue := FCurrentValue / 100;

FResultEdit.Text := FloatToStr(FCurrentValue);

end;

btSqrt:

begin

FCurrentValue := StrToFloat(FResultEdit.Text);

FCurrentValue := Sqrt(FCurrentValue);

FResultEdit.Text := FloatToStr(FCurrentValue);

end;

btMemoryStore:

begin

FMemoryValue := StrToFloat(FResultEdit.Text);

FMemoryValue := FMemoryValue * 1;

FCurrentValue := 0;

end;

btMemoryAdd:

begin

TempValue := FMemoryValue;

FMemoryValue := StrToFloat(FResultEdit.Text);

FMemoryValue := (FMemoryValue * 1) + TempValue;

end;

btMemoryRecall:

begin

FResultEdit.Text := FloatToStr(FMemoryValue);

FCurrentValue := 0;

end;

btMemoryClear:

begin

FMemoryValue := 0;

end;

btEquals:

if FState <> csNone then begin

FBackSpaceValid := False;

FCurrentValue := StrToFloat(FResultEdit.Text);

if FRepeatValue = 0 then begin

FRepeatValue := FCurrentValue;

FCurrentValue := FLastValue;

end;

FLastValue := FRepeatValue;

case FState of

csAdd:

FCurrentValue := FCurrentValue + FLastValue;

csMultiply:

FCurrentValue := FCurrentValue * FLastValue;

csSubtract:

FCurrentValue := FCurrentValue - FLastValue;

csDivide:

FCurrentValue := FCurrentValue / FLastValue;

end;

FLastValue := FCurrentValue;

FResultEdit.Text := FloatToStr(FCurrentValue);

FCurrentValue := 0;

end;

end; // case ButtonType of...

except

on E: Exception do begin

FResultEdit.Text := E.Message;

FLastValue := 0;

FCurrentValue := 0;

FRepeatValue := 0;

FState := csNone;

end;

end;

FStatus.Caption := MemoryStoreMap[FMemoryValue = 0];

end;

function TCalculator.GetText: string;

begin

Result := FResultEdit.Text;

end;

procedure TCalculator.SetText(const Value: string);

begin

FResultEdit.Text := Value;

end;

end.

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有