集成 Windows 本地应用到 Eclipse RCP 程序中

原文:http://www.ibm.com/developerworks/cn/opensource/os-cn-eclrcp/index.html?ca=drs-cn-0605


Windows 应用程序非常丰富,而有时我们的 Eclipse RCP 程序所需要的一些功能已经有一些现有的 Windows 本地应用程序的实现,我们希望能够在我们的 RCP 程序中重用这些功能。一种最简单的重用方法就是直接在我们 RCP 窗口中嵌入本地应用程序窗口。要使得一个 Windows 本地应用程序能够在我们的 RCP 程序中运行,我们可以使用 Windows 提供的 reparent 机制。利用这种机制实现窗口嵌入的主要过程是:首先要在我们的程序中启动要嵌入的 Windows 程序,然后我们设法获取程序启动后的主窗口句柄,再将我们RCP程序的窗口设置成 Windows 程序主窗口的父窗口。

由于我们需要启动 Windows 本地程序并且获取它的主窗口句柄,这些只能使用 Windows 本地调用来实现,所以我们先用 Windows 本地调用实现相应的功能,然后我们再用 JNI 进行调用。

JNI 简介

JNI 的全称是 Java Native Interface,JNI 标准是 Java 平台的一部分,它用来将 Java 代码和其他语言写的代码进行交互。下面简单介绍一下使用 JNI 的步骤:

编写带有 native 声明的 java 方法

这里以 HelloWorld 为例:


清单 1. Hello World Java 代码
                
public class HelloWorld {
static {
System.loadLibrary(“helloworld”);
}

public native void print();

public static void main(String[] args) {
HelloWorld hello = new HelloWorld();
hello.print();
}
}

编译 Java 代码以及生成 c/c++ 头文件:

先编译这个 java 类: javac HelloWorld.java,然后再生成扩展名为 .h 的头文件,java 提供了命令 javah 来生成头文件:javah –jni HelloWorld,下面的清单显示了生成的头文件的内容:


清单 2. Hello World C++ 头文件
                
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: print
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

使用 c/c++ 实现本地方法并编译成动态库文件

前面已经生成了 c/c++ 的头文件,下面要实现头文件中声明的函数,具体的实现代码如下面的清单所示,示例代码中仅仅是输出一行文字“HelloWorld”:


清单 3. Hello World C++ 实现代码
                
#include "HelloWorld.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv * env, jobject obj)
{
printf("Hello World");
}

接下来要做的就是将这个 c++ 的代码编译成动态库文件,在 HelloWorld.cpp 文件目录下面,使用 VC 的编译器 cl 命令来编译:

cl -I%java_home%\include -I%java_home%\include\win32 -LD HelloWorld.cpp –Fehelloworld.dll

注 意:生成的 dll 文件名在选项 -Fe 后面配置,这里是 helloworld.dll,因为前面我们在 HelloWorld.java 文件中 loadLibary 的时候使用的名字是 helloworld。所以要保证这里的名字和前面 load 的名字一致。另外需要将 -I%java_home%\include -I%java_home%\include\win32 参数加上,因为在第四步里面编写本地方法的时候引入了 jni.h 文件,所以在这里需要加入这些头文件的路径。

完成了这些步骤之后就可以运行这个程序:java HelloWorld,运行的结果就是在控制台输出字符串“HelloWorld”。

实现窗口 Reparent

前面部分介绍了如何使用 JNI,接下来介绍如何通过 JNI 启动一个 Windows 的本地应用程序并且将其主窗口设置为指定窗口的子窗口。首先创建一个 Java 类,如下面的清单所示:

public class ReparentUtil {
static{
System.loadLibrary("reparent");
}
public static native int startAndReparent(int parentWnd,
String command,String wndClass);
}

其中 System.loadLibrary("reparent") 是用来加载名为 reparent 的动态库,我们会在这个动态库中具体实现方法 startAndReparent(…)。

startAndReparent 定义方法来启动 Windows 程序,并且将其窗口 reparent 到我们指定的窗口。其中:

  • int parentWnd: 父窗口句柄
  • String command:Windows 程序启动命令
  • String wndClass:Windows 程序主窗口类型

由于有的程序启动后会创建多个顶级窗口,所以我们在这里要指定一个主窗口类型来区分不同的顶级窗口。这个方法是一个本地方法,我们会用 C++ 生成为一个叫 reparent.dll 的动态库,这个方法即存在于这个动态库中。

这 个 Java 函数对应的的 C++ 函数是 Java_com_reparent_ReparentUtil_startAndReparent(JNIEnv *env, jclass classobj, jint parent, jstring command, jstring wndClass), 这个函数主要实现两部分的功能:

  • 启动 Windows 应用程序;
  • 获取 Windows 应用程序的主窗口句柄;
  • 将 Windows 应用主窗口设置成指定窗口的子窗口。

启动 Windows 应用程序

下面我们来看看启动 Windows 应用程序的实现. 我们先将函数传入的 Java 字符串参数转化成 C 字符串。这个过程主要通过 GetStringChars() 来实现。

JNIEXPORT jint JNICALL Java_com_reparent_ReparentUtil_startAndReparent
(JNIEnv *env, jclass classobj, jint parent, jstring command,
jstring wndClass){
jboolean isCopy=FALSE;
PROCESS_INFORMATION pInfo;
STARTUPINFO sInfo;

int hParentWnd;

jsize len = ( *env ).GetStringLength(command);
const jchar *commandstr = (*env).GetStringChars(command,&isCopy);
const jchar *wndClassStr = NULL;
char commandcstr[200];
int size = 0;
size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)commandstr,
len, commandcstr,(len*2+1), NULL, NULL );
(*env).ReleaseStringChars(command, commandstr);
if(size==0){
return 0;
}
commandcstr[size] = 0;

if(wndClass!=NULL){
wndClassStr = (*env).GetStringChars(wndClass,&isCopy);
if(wndClassStr!=NULL){
len = (*env).GetStringLength(wndClass);
size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)wndClassStr,
len, wndClassName,(len*2+1), NULL, NULL );
wndClassName[size] = 0;
(*env).ReleaseStringChars(wndClass, wndClassStr);
}
}

接着,我们使用 Windows 的 API:CreateProcess 函数来启动我们要集成的应用程序。

sInfo.cb                     =   sizeof(STARTUPINFO);   
sInfo.lpReserved = NULL;
sInfo.lpReserved2 = NULL;
sInfo.cbReserved2 = 0;
sInfo.lpDesktop = NULL;
sInfo.lpTitle = NULL;
sInfo.dwFlags = 0;
sInfo.dwX = 0;
sInfo.dwY = 0;
sInfo.dwFillAttribute = 0;
sInfo.wShowWindow = SW_HIDE;

if(!CreateProcess(NULL,commandcstr,NULL,NULL, TRUE,0,NULL,NULL,&sInfo,&pInfo))
{
printf("ERROR: Cannot launch child process\n");
release();
return 0;
}

CreateProcess 函数的定义是:

BOOL CreateProcess (
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes。
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

其中 lpApplicationName:指向一个 NULL 结尾的、用来指定可执行模块的字符串。lpCommandLine:指向一个 NULL 结尾的、用来指定要运行的命令行。lpProcessAttributes: 指向一个 SECURITY_ATTRIBUTES 结构体,这个结构体决定是否返回的句柄可以被子进程继承。lpThreadAttributes: 指向一个 SECURITY_ATTRIBUTES 结构体,这个结构体决定是否返回的句柄可以被子进程继承。bInheritHandles:指示新进程是否从调用进程处继承了句柄。 dwCreationFlags:指定附加的、用来控制优先类和进程的创建的标志。lpEnvironment:指向一个新进程的环境块。 lpCurrentDirectory:指向一个以 NULL 结尾的字符串,这个字符串用来指定子进程的工作路径。lpStartupInfo:指向一个用于决定新进程的主窗体如何显示的 STARTUPINFO 结构体。lpProcessInformation:指向一个用来接收新进程的识别信息的 PROCESS_INFORMATION 结构体。

获取应用程序的主窗口句柄

为了获取启动后的程序的主窗口句柄,在调用 CreateProcess() 之前,我们需要使用一个 Windows 的系统钩子来截获窗口创建的事件:

hHook = SetWindowsHookEx(WH_SHELL, ShellProc,(HINSTANCE)hDllHandle,NULL);

这里,我们使用的钩子类型是 WH_SHELL。这种钩子可以截获所有顶级窗口创建或者激活的事件。函数的第二个参数是事件处理函数。我们的处理函数叫 ShellProc。我们之后会介绍。

启动应用程序之后,我们需要获取应用程序的主窗口之后才能继续运行。这里需要实现进程间的同步。在我们的主进程中,我们需要等待,当应用程序的主窗口创建之后,我们发一个消息,通知我们的主进程继续执行。

我 们这里使用 Windows 的 Event 来实现同步。我们首先调用 CreateEvent 来创建一个事件,然后调用 WaitForSingleObject()等待事件的状态改变。在我们的 ShellProc 处理函数中,我们一旦获取应用程序主窗口句柄,我们会改变事件的状态以通知主进程继续执行。

以下是创建事件的代码,我们创建了一个名为 Global\WaitWindowCreatedEvent 的事件:

	SECURITY_ATTRIBUTES secuAtt;
secuAtt.bInheritHandle = TRUE;
secuAtt.lpSecurityDescriptor = NULL;
secuAtt.nLength = sizeof(SECURITY_ATTRIBUTES);
hEvent = CreateEvent(&secuAtt,FALSE,FALSE,TEXT("Global\WaitWindowCreatedEvent"));

等待事件状态变化可以调用以下代码:

	WaitForSingleObject(hEvent,1000*60);

为了避免无限的等待下去,我们设置了一个最长的等待时间,为60秒。

下 面我们再来看 ShellProc 的处理代码。这个函数中,我们主要是要获取应用程序的主窗口。根据 Windows 系统 WH_SHELL 钩子的定义,钩子的处理函数的第一个参数是事件类型,第二个参数是窗口句柄。我们首先判断窗口的类型是否是 HSHELL_WINDOWCREATED,然后判断对应窗口所属的进程号是否等于我们所启动的应用程序,如果需要还要判断窗口类型。一旦我们找到了应用 程序主窗口,我们通过调用 SetEvent 来通知主进程继续执行。

	LRESULT CALLBACK ShellProc(int nCode,WPARAM wParam,LPARAM lParam){
if(nCode==HSHELL_WINDOWCREATED && childInstanceId!=0){
HWND hwnd=HWND(wParam);
DWORD pid;
HANDLE childEvent;
char classname[100];
GetWindowThreadProcessId(hwnd,&pid);
if(pid==childInstanceId){
if(wndClassName[0]!=0){
int count = GetClassName(hwnd,classname,100);
classname[count] = 0;
if(strcmp(classname,wndClassName)!=0){
return CallNextHookEx(hHook, nCode,
wParam, lParam);
}
}
hChildWnd = hwnd;
ShowWindow(hChildWnd,SW_HIDE);
childEvent = OpenEvent(EVENT_ALL_ACCESS,
TRUE,TEXT("Global\WaitWindowCreatedEvent"));
if(childEvent!=0){
SetEvent(childEvent);
}
}
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}

将 Windows 应用主窗口设置成指定窗口的子窗口

获 取应用程序的主窗口句柄之后,在 Java_com_reparent_ReparentUtil_startAndReparent 函数的最后,我们通过调用 Windows 的 SetParent 函数将其设置成我们的子窗口,同时调整一下应用程序窗口的大小以使其能刚好显示在我们的窗口中。为了避免窗口的闪烁,我们先将窗口隐藏,reparent 之后再显示。为了去掉应用程序的窗口栏,我们需要将应用程序的窗口类型改为 WS_POPUP。

if(hChildWnd!=0){
RECT rect;
GetWindowRect((HWND)hParentWnd,&rect);
ShowWindow(hChildWnd,SW_HIDE);
SetParent(hChildWnd,(HWND)hParentWnd);
SetWindowPos(hChildWnd,(HWND)0,0,0,
rect.right-rect.left,rect.bottom-rect.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS |
SWP_SHOWWINDOW | SWP_NOSENDCHANGING | SWP_DEFERERASE);
SetWindowLong(hChildWnd,GWL_STYLE,WS_POPUP);
ShowWindow(hChildWnd,SW_SHOW);
}

包装 Windows 应用程序窗口到 SWT 控件

实 现了 startAndReparent 方法后,只要将我们 SWT 窗口句柄传入,我们就可以将一个 Windows 本地应用嵌到我们的 SWT 窗口中了。为了方便使用,我们可以将 Windows 本地应用包装到一个 SWT Control 中,这样我们就可以象使用普通 SWT Control 一样使用 Windows 应用程序的窗口。下面我们来看如何实现对 Windows 应用程序窗口的包装。

首先我们定义一个 Control,它从 Canvas 继承而来。我们用它来作为本地应用程序窗口的父窗口,同时实现对它的管理。我们主要要实现以下几个方面的管理:

  • 窗口的创建:当我们 SWT 窗口创建时,我们需要将本地应用程序窗口创建出来
  • 窗口的销毁:当我们 SWT 窗口销毁时,我们也要将本地应用程序窗口销毁。
  • 焦点控制:当我们的 SWT 窗口获取到焦点时,我们要将焦点设置到本地应用程序窗口中。
  • 窗口大小的变化:当我们的 SWT 窗口的位置或大小发生变化时,我们要通知本地应用程序窗口改变它的位置或大小。

首 先我们来看窗口的创建和销毁。我们需要监听 SWT 窗口的 Paint 事件和 Dispose 事件,在响应 Paint 事件中创建本地应用程序窗口,在响应 Dispose 事件中关闭本地应用程序窗口。需要注意的是,我们创建本地应用窗口可能需要花较长的时间,为了避免阻塞 UI 线程,我们将其放在一个线程中执行。如下面的清单所示:

public class NativeControl extends Canvas{
private int childWnd = 0;
private String startCommand = null;
private String wndClassName = null;

private boolean isCreatingNative = false;

public NativeControl(Composite parent, int style) {
super(parent, style);
this.addPaintListener(new PaintListener(){

public void paintControl(PaintEvent arg0) {
this.addPaintListener(new PaintListener(){

public void paintControl(PaintEvent arg0) {
if(childWnd==0 && !isCreatingNative){
isCreatingNative = true;
Thread thread = new Thread(){
public void run(){
childWnd = ReparentUtil.startAndReparent(
NativeControl.this.handle,startCommand,wndClassName);

}
};
thread.start();
}
}
});
}
});
this.addDisposeListener(new DisposeListener(){

public void widgetDisposed(DisposeEvent arg0) {
if(childWnd!=0){
OS.SendMessage(childWnd, OS.WM_CLOSE, 0, 0);
}
}

});

在 paintControl(PaintEvent arg0) 函数中调用 ReparentUtil.startAndReparent(NativeControl.this.handle,startCommand,wndClassName) 来启动 Windows 应用程序并将应用程序窗口显示到 SWT 控件中。当 SWT 空间销毁的时候也要将 Windows 应用程序的窗口销毁。SWT 的 OS 类提供了 SendMessage 方法来实现将窗口销毁:OS.SendMessage(childWnd, OS.WM_CLOSE, 0, 0);childWnd 就是要销毁的窗口的句柄。

窗口焦点的控制和窗口的销毁比较类似,我们先监听父窗口的焦点事件,一旦获取焦点,我们将焦点设置到本地应用程序的窗口中。同时,我们需要加一个键盘事件监听器,这样当用户按“Tab”键时,焦点才能跳转到我们的父窗口控件。如下面的清单所示:

		this.addFocusListener(new FocusListener(){

public void focusGained(FocusEvent arg0) {
if(childWnd!=0){
OS.SetForegroundWindow(childWnd);
}
}

public void focusLost(FocusEvent arg0) {

}

});
this.addKeyListener(new KeyListener(){

public void keyPressed(KeyEvent arg0) {


}

public void keyReleased(KeyEvent arg0) {


}

});

SWT 的 OS 类提供了 SetForegroundWindow 函数来将焦点设置到某个窗口上,函数的参数指定要设置焦点的窗口句柄。

窗口的大小的控制也是类似的。我们需要监听父窗口的窗口事件,一旦有窗口大小变化,我们就调整本地应用程序的窗口大小。

this.addControlListener(new ControlListener(){

public void controlMoved(ControlEvent arg0) {

}

public void controlResized(ControlEvent arg0) {
if(childWnd!=0){
Rectangle rect = ((Composite)(arg0.widget)).getClientArea();
OS.SetWindowPos(childWnd, 0, rect.x, rect.y, rect.width, rect.height,
OS.SWP_NOZORDER| OS.SWP_NOACTIVATE | OS.SWP_ASYNCWINDOWPOS);
}
}

});

同样的我们利用 SWT 提供的函数来设置窗口的大小和位置,SetWindowPos 的参数分别是要设置的窗口句柄以及窗口位置大小。

最后我们需要添加一些方法,让用户可以设置启动应用程序的命令以及应用程序的窗口类型。

	public void setStartParameters(String startCommand,String wndClassName){
this.startCommand = startCommand;
this.wndClassName = wndClassName;
}

public String getStartCommand() {
return startCommand;
}



public void setStartCommand(String startCommand) {
this.startCommand = startCommand;
}



public String getWndClassName() {
return wndClassName;
}



public void setWndClassName(String wndClassName) {
this.wndClassName = wndClassName;
}

这样我们就开发了一个 SWT 的控件,它可以将指定的 Windows 本地应用程序启动并将程序的窗口嵌入到控件中。对这个控件的使用和普通 SWT 的控件一样,唯一的区别就是要在窗口显示前调用 setStartParameters() 方法设置 Windows 本地应用程序的启动命令和窗口的类型。

下面是一个简单的例子,把 Windows Messager 嵌入到了我们的 SWT 的窗口中。

public class ReparentTest {

/**
* @param args
*/
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("Test dialog");
GridLayout layout = new GridLayout();
layout.numColumns = 1;
shell.setLayout(layout);

Button button = new Button(shell,SWT.None);
button.setLayoutData(new GridData());
button.setText("Test");
NativeControl control = new NativeControl(shell,SWT.NONE);
GridData data = new GridData(GridData.FILL_BOTH);
data.widthHint = 200;
data.heightHint = 200;
data.grabExcessHorizontalSpace = true;
data.grabExcessVerticalSpace = true;
control.setLayoutData(data);
control.setStartParameters
("C:\\Program Files\\Messenger\\Msmsgs.exe","MSBLClass");
shell.open();
while(!shell.isDisposed()){
if(!display.readAndDispatch()){
display.sleep();
}
}
}

}

通过 setStartParameters() 方法来设置要启动的程序的路径以及该程序的窗口类型,在这里我们启动 MSN,对应的窗口类型是 MSBLClass:

control.setStartParameters("C:\\Program Files\\Messenger\\Msmsgs.exe","MSBLClass");

以下是代码显示的结果。我们可以拉伸改变窗口的大小,这时里面的 Messager 的窗口大小也会随之而变化。当焦点在 Test 按钮上时,按“Tab”键,焦点也会跳转到 Messager 的窗口上。


图 1. 图片示例
图片示例

小结

本 文介绍了将一个本地应用程序窗口集成到 Eclipse RCP 窗口中的相关技术。文中主要讨论的集成第三方的应用程序,由于我们不掌握第三方应用程序的代码,这种集成方式还是比较简单。例如本地应用程序的菜单还是显 示在我们的SWT父窗口中,而不是显示在 Eclipse RCP 应用程序的主菜单中。有时,我们也需要将我们自己开发本地应用程序集成到 Eclipse RCP 程序中。其实现原理也和本文讲述的一样。不同的是,我们可以实现更多的对我们本地应用程序的控制,从而实现更紧密的集成。例如,我们的本地应用程序可以提 供 API 让 RCP 程序获取自己的主菜单,并且将其主菜单显示在 RCP 程序的主菜单中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
开发项目用SWING与RCP与SWT.JFACE的分析 第一个SWT程序 下面让我们开始一个SWT程序。(注意:以下的例子和说明主要针对Windows平台,其它的操作系统应该大同小异)。首先要在Eclipse安装文件中找到SWT包,Eclipse组织并不提供单独的SWT包下载,必须下载完整的Eclipse开发环境才能得到SWT包。SWT是作为Eclipse开发环境的一个插件形式存在,可以在${你的eclipse安装路径}\plugins路径下的众多子目录下去搜索SWT.JAR文件,在找到的JAR文件中包含了SWT全部的Java类文件。因为SWT应用了JNI技术,因此同时也要找到相对应的JNI本地化库文件,由于版本和操作平台的不同,本地化库文件的名称会有些差别,比如SWT-WIN32-2116.DLL是Window平台下Eclipse Build 2116的动态库,而在Unix平台相应版本的库文件的扩展名应该是.so,等等。注意的是,Eclipse是一个开放源代码的项目,因此你也可以在这些目录中找到SWT的源代码,相信这会对开发很有帮助。下面是一段打开空窗口的代码(只有main方法)。 import com.e2one.example; public class OpenShell{ public static void main(String [] args) { Display display = new Display(); Shell shell = new Shell(display); shell.open(); // 开始事件处理循环,直到用户关闭窗口 while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } } 确信在CLASSPATH中包括了SWT.JAR文件,先用Javac编译例子程序。编译无错后可运行java -Djava.library.path=${你的SWT本地库文件所在路径} com.e2one.example.OpenShell,比如SWT-WIN32-2116.DLL件所在的路径是C:\swtlib,运行的命令应该是java -Djava.library.path=c:\swtlib com.e2one.example.OpenShell。成功运行后,系统会打开了一个空的窗口。 剖析SWT API 下面再让我们进一步分析SWT API的组成。所有的SWT类都用org.eclipse.swt做为包的前缀,下面为了简化说明,我们用*号代表前缀org.eclipse.swt,比如*.widgets包,代表的是org.eclipse.swt.widgets包。 我们最常用的图形构件基本都被包括在*.widgets包中,比如Button,Combo,Text,Label,Sash,Table等等。其中两个最重要的构件当数Shell和Composite。Shell相当于应用程序的主窗口框架,上面的例子代码中就是应用Shell构件打开一个空窗口。Composite相当于SWING中的Panel对象,充当着构件容器的角色,当我们想在一个窗口中加入一些构件时,最好到使用Composite作为其它构件的容器,然后再去*.layout包找出一种合适的布局方式。SWT对构件的布局也采用了SWING或AWT中Layout和Layout Data结合的方式,在*.layout包中可以找到四种Layout和与它们相对应的布局结构对象(Layout Data)。在*.custom包中,包含了对一些基本图形构件的扩展,比如其中的CLabel,就是对标准Label构件的扩展,上面可以同时加入文字和图片,也可以加边框。StyledText是Text构件的扩展,它提供了丰富的文本功能,比如对某段文字的背景色、前景色或字体的设置。在*.custom包中也可找到一个新的StackLayout布局方式。 SWT对用户操作的响应,比如鼠标或键盘事件,也是采用了AWT和SWING中的Observer模式,在*.event包中可以找到事件监听的Listener接口和相应的事件对象,例如常用的鼠标事件监听接口MouseListener,MouseMoveListener和MouseTrackListener,及对应的事件对象MouseEvent。 *.graphics包中可以找到针对图片、光标、字体或绘图的API。比如可通过Image类调用系统中不同类型的图片文件。通过GC类实现对图片、构件或显示器的绘图功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值