在 Java 中嵌入 .NET 控件






4.97/5 (27投票s)
学习如何使用 COM 在 Java 应用程序、小程序和 bean 中嵌入 .NET 用户控件,从而弥合两个框架之间的鸿沟。
引言
Microsoft .NET 控件为开发人员提供了构建模块化解决方案所需的优势。开发人员可以从无数现有控件中设计自定义控件,并将自定义控件嵌入到 Windows 窗体或另一个控件中。通过一些额外的代码,还可以将这些控件嵌入到 Web 窗体中。在这些更改之外编写一个额外的简单库将允许您在 Java 应用程序或小程序中运行该控件。
你可能会问:“为什么会有人想在 Java 中嵌入 .NET 控件?”。使用单一语言解决复杂问题通常是不可能的——尤其是在处理遗留应用程序时。COM 通过允许以多种语言和模块化方式编写解决方案来简化此类问题的解决。一位精通 Visual Basic 的开发人员可以用 VB 编写组件,而另一位精通 C++ 的开发人员可以使用 ATL 编写组件。这种模块化架构还允许开发人员在单个项目中并肩工作,并实现更好的版本控制,因为一个“模块”中的微小更改不需要重新编译和重新分发整个解决方案。当我们公司的母公司表示希望将我们的 .NET 组件嵌入到他们的遗留 Java 应用程序中时,我知道良好的模块化架构和 COM 互操作性是正确的选择。
本教程将涵盖许多内容,包括正确地将 .NET 控件作为 COM 对象发布、实例化和控制所需的 ATL(活动模板库)基本类、Java 本机接口(JNI)的基本信息,以及使用上述技术将 .NET 控件包装到 Java 中。
尽管我们的 .NET 控件最终目标是 JavaBeans,但我希望您在本教程中能看到许多可以引导您扩展解决方案并达到不同目标的地方。例如,一旦您学会了如何使用 ATL 在 C/C++ 中实例化 .NET 控件,您就可以创建一个简单的 Win32 或 MFC Windows 应用程序来托管该控件。
无论如何,我确信本教程将有助于您理解 .NET 中用于运行时可调用包装器 (CRW) 的正确 COM 实现,以及使用 C/C++ 和 ATL 将控件实例化为 COM 对象。
必备组件
在踏上艰难的道路之前,重要的是要理解基本概念并在您的计算机上安装各种框架和工具。
- 您将需要 Microsoft .NET Framework 软件开发工具包 (SDK)。
- 您将需要 Sun Java 开发工具包 (JDK)。我推荐 Java 1.4.0。
- 可选地,您可能希望使用 .NET 集成开发环境 (IDE),例如 Microsoft Visual Studio .NET。
- 可选地,您可能希望使用 Java IDE。我推荐 Sun ONE Studio 4 CE,以前称为 Forte for Java CE。它是免费的,功能强大,与 Java 1.4.0 捆绑在一起,并且由编写 Java 的人编写。
基本概念
如果你认为你从未接触过 COM,那你就错了。自 Windows 95 以来,COM 一直是 Windows 不可或缺的一部分。从桌面到任务栏,从菜单到工具栏,以及更多的一切都围绕着 COM 展开。但什么是 COM?COM 代表 Component Object Model(组件对象模型),这意味着你可以以特定方式构建代码片段,使所有片段都能协同工作。例如,包含无处不在的文本框、标签等的 Microsoft Common Controls 都是 COM 对象。当你在 VB6 中编程时,你使用的几乎所有东西都是 COM 对象。COM 还呈现了一个自然的客户端/服务器架构,其中客户端提供服务器消费的功能。Internet Explorer 是一个经典的例子,其中 WebBrowser 控件(IWebBrowser2 接口)提供浏览功能,而包含菜单、工具栏、资源管理器栏、状态栏等的应用程序则消费该功能。
然而,在我深入研究 COM 之前,请记住 COM 对象几乎可以在任何语言中使用。每个 COM 对象都实现 IUnknown
(就像每个 .NET 类都隐式扩展 System.Object
一样),它包含三个方法:AddRef
、QueryInterface
和 Release
。AddRef
和 Release
增加和减少 DLL 的引用计数。当 DLL 第一次使用时,它被加载到内存中。在随后的实例化中,调用 AddRef
并且引用计数增加。当服务器完成客户端的工作时,调用 Release
并且引用计数减少。当引用计数达到零 (0) 时,COM 对象销毁自身。QueryInterface
用于检查和获取 COM 对象可能也实现的特定接口,例如 IObjectSafety
、IPeristStorage
以及许多其他接口。
因此,既然我们有一种方法可以在几乎任何语言中托管控件,那么使用 COM 似乎是托管非原生 Java 控件的合理选择。由于我们还可以将 .NET 控件公开为 COM 对象,因此此解决方案变得更具吸引力。
但是,如何将 COM 对象嵌入到 Java 中呢?Java 本机接口 (JNI) 是 Java 使用 native
方法的一种方式,通常在 C++ 应用程序中。由于我们正在处理 C++,我们可以轻松实例化 COM 对象。JNI 方法在 DLL 中从 Java 本机方法调用,然后可以创建一个窗口来托管 COM 控件,并将该窗口附加到窗口句柄或 HWND
。由于 Windows 中的每个窗口都有一个 HWND
,我们只需要通过一个未公开的方法从 Java 获取一个 HWND
,我们就可以做任何事情——包括在 Java 应用程序或小程序中托管我们作为 COM 对象公开的 .NET 控件!
构建我们的 .NET 控件
首先,让我们从一个简单的 .NET 用户控件开始。由于本教程的范围不一定包括在 Web 上托管此类控件,我将不讨论 Web 托管用户控件所需的某些事项。涵盖此类工作的特定限制的教程将在以后发布。
创建一个新的 Windows 控件库(本例将使用 C#,尽管它可以轻松移植到 VB.NET),并将其命名为“COMTest”。项目创建后,将 UserControl1.cs 重命名为 MyCOMObject.cs。对文件名(选择文件本身并检查 PropertyGrid)和控件(选择控件本身并检查 PropertyGrid)都执行此操作。继续并在上面放置一些控件。本教程中您在控件上放置什么并不重要,我之所以让您按我上面提到的名称命名,是因为该名称将经常出现,我不想过于模糊。
有关示例 .NET 控件,请参见演示项目。这是一个基本的用户控件,它接受输入值并向用户显示“Hello”消息。这是一个足够简单的示例。但是,我想添加一个事件,以便包含控件知道内部按钮何时被单击,并可以从文本框获取值。我将实现一个简单的事件(甚至不类似于 EventHandler
委托),并公开文本框的文本。
为此,添加一个名为 UserName 的公共属性,类型为 String
[Category("Data")]
[Description("The name displayed in the \"Your Name:\" text box.")]
public string UserName
{
get { return this.NameBox.Text; }
set { this.NameBox.Text = value; }
}
另外,在类的上方,添加以下公共委托
public delegate void HelloClicked();
这是一个简单的事件委托,在任何 EventArgs
类和子类难以表示的情况下都易于处理。
然后,我们将受保护的 On<Event> 方法和事件本身添加到代码中,并通过在 Button.Click
处理程序内部调用受保护的 On<Event> 方法来引发事件。
[Category("Action")]
[Description("Occurs after the \"Say Hello\" button is clicked.")]
public event HelloClicked Clicked;
protected virtual void OnClicked()
{
if (this.Clicked != null)
this.Clicked();
}
最后,在设置值后,在 UserName 属性的 set 访问器中添加以下内容,这将调用上面引发 Clicked
事件的受保护方法
OnClicked();
最终代码位于 demo project 的 COMTest 文件夹中。我们将在下一节添加 COM 互操作功能时,添加类中包含的其余代码。然而,现在存在的代码可以完全嵌入到其他 .NET 控件中。
将控件公开为 COM 组件
信不信由你,将这个 .NET 控件公开为 COM 对象所需的工作量非常少。但在我深入探讨之前,我想解释一些关于互操作的事情。
首先,我们不希望 .NET 编译器生成类接口——COM 服务器实际“对话”的接口——因为存在许多问题,包括版本控制和 VTABLE 排序,VTABLE 排序是出现在 VTABLE 中的函数顺序,其中包含有关类本身的信息。如果我们允许 .NET 编译器这样做,则很难维护一致的类接口,因为方法顺序可能会更改,而 COM 服务器期望函数位于某些地址。这被称为调度。使用 IDispatch
接口,服务器可以找出 COM 客户端中可用的方法及其在 VTABLE 中的相应索引。如果此方法位置更改,我们的 COM 服务器将调用错误的方法!
其次,我们需要为我们的 COM 对象提供强名称,这意味着我们必须为程序集生成一个公钥,并为程序集提供一些程序集级别的属性。这允许我们将 .NET 控件插入到全局程序集缓存 (GAC) 中,这意味着更快的加载时间,特别是如果我们将它添加为本机程序集 (ngen -i <Assembly>)。
让我们先处理简单部分:为程序集强命名。首先,在项目目录中运行“sn -k KeyFile.snk”。其次,打开 AssemblyInfo.cs 文件,并确保填写了 AssemblyTitle
、AssemblyVersion
和 AssemblyKeyFile
属性。我们还可以在这里定义一个 GUID,它将是标识类型库的 GUID,我稍后会详细介绍。我的 AssemblyInfo.cs 文件最终看起来像这样
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("My COM Test")]
[assembly: AssemblyDescription("COM Test Library")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("CodeProject")]
[assembly: AssemblyProduct("COMTest")]
[assembly: AssemblyCopyright("Copyright 2002")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(@"..\..\KeyFile.snk")]
[assembly: AssemblyKeyName("")]
[assembly: Guid("B58D7C8C-2E2D-4aa6-8EAF-CF7CB448E353")]
您可以使用 VS.NET 的“工具”菜单中的“创建 GUID”工具生成 GUID,或者从命令行/运行提示符运行“guidgen”。
现在,我们将做稍微困难一点的部分,尽管它也没那么糟糕。我还想提一下,您应该对任何不希望公开为 COM 对象的类型使用 ComVisibleAttribute
。这些通常包括模态窗体和仅在项目内部使用的内部用户控件,例如从您作为 COM 公开的控件打开的模态对话框。为此,只需在任何类、结构或接口上使用 ComVisible(false)
。这也是排他性的,因此您可以在程序集级别使用此属性,并对要公开的任何类使用“true”参数。我不使用此方法,因为此项目只包含我的 COM 组件,我想公开几乎所有内容。例如,我确实在我的委托上使用了 ComVisibleAttribute
,这样它本身就不会显示为 COM 对象
[ComVisible(false)]
public delegate void HelloClicked();
无论如何,回到类接口。类接口是 COM 客户端实际使用的东西。它知道包含其想要使用的功能的接口,以及实现该接口的类的 CLSID(类标识符,一个全局唯一标识符,即 GUID)。使用这种方法,COM 客户端对控件(COM 服务器)的位置、语言或实现一无所知,只知道它包含某些功能。实质上,COM 客户端对服务器的调用的简单文本图示如下
client --> [interface --> server]
客户端在接口上调用一个方法,但运行时正在将该调用封送到实现该接口的类。
因此,当我们生成类接口时,我们将希望公开 COM 客户端可能使用的任何方法、属性或事件。首先,我们将定义事件接口,其中只包含我们想要公开的事件。在这种情况下,只有 Click
事件。属性也会附加,并将在代码片段之后进行解释。
所以,打开 MyCOMObject.cs 的代码,并在命名空间声明之后在顶部添加以下内容
[Guid("70B9F4F4-0285-4aae-B64E-DE57BDBF49C5")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface DMyCOMObject
{
void Clicked();
}
GuidAttribute
是接口的另一个 GUID。COM 对象名称前加上字母“D”是事件接口的标准命名约定。InterfaceTypeAttribute
将此接口声明为调度接口(dispinterface
),这通常是事件接口。
接下来,我们公开了几个用于绘制控件的继承属性和方法以及 UserName 属性,以便可以从 COM 客户端访问它们。
Guid("CAE73FF2-2D47-4677-B8EA-3E0FF12E4B0D")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IMyCOMObject
{
Color BackColor { get; set; }
Color ForeColor { get; set; }
int Top { get; set; }
int Left { get; set; }
int Width { get; set; }
int Height { get; set; }
IntPtr Handle { get; }
bool Visible { get; set; }
void Show();
void Hide();
void Refresh();
void Update();
string UserName { get; set; }
}
同样,我们为此接口定义一个 GUID,并用字母“I”作为接口前缀,这对于一般接口和类接口都是常见的,您应该记住,类接口包含 COM 对象公开的属性和方法。前许多都继承自 System.Windows.Forms.Control
,而最后一个是 MyCOMObject 本身的。
最后,我们向 MyCOMObject 类添加一个 GuidAttribute
,以及其他一些属性,如下所示
[Guid("F65B3579-FEAA-4da5-BABA-1B9D195307FF")]
[ComSourceInterfaces(typeof(DMyCOMObject))]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("COMTest.MyCOMObject")]
public class MyCOMObject : System.Windows.Forms.UserControl, IMyCOMObject
{
// ...
}
ComSourceInterfaceAttribute
定义了此 COM 对象的事件接口。ClassInterfaceAttribute
告诉编译器不要为此类生成类库,而是使用实现接口,您会注意到我们添加了 IMyCOMOBject 作为此类实现的接口。最后,我们添加了一个 ProgId,这是引用此控件的一种简单方法。有与版本无关的 ProgId,如上所述,也有与版本相关的 ProgId,例如“COMTest.MyCOMObject.1.0”,.NET 实际上根据 AssemblyVersionAttribute
值(仅主版本.次版本)将其添加到注册表中。
最终代码可在 demo project 的 COMTest 文件夹中找到。
现在打开项目属性,确保所有构建的“注册 COM 互操作”为“true”,然后构建您的项目。如果您希望从命令行构建您的项目,可以使用 regasm.exe 运行以下命令
regasm.exe /codebase COMTest.dll
如果您为程序集提供了强名称(使用私钥签名并指定了版本号),您可以将该程序集添加到全局程序集缓存中,并运行 regasm.exe 注册该程序集,这样客户端将使用 GAC 中的版本,而不是依赖于程序集的代码库
gacutil /i COMTest.dll
regasm.exe COMTest.dll
编写 Java 应用程序
在实际编写 JNI 包装器之前,我们需要从 Java 类生成一个头文件。由于这个 Java 类还不存在,下一个合乎逻辑的步骤是创建我们的 Java 类。为简单起见,我将开发一个简单的基于 java.awt.Frame 的应用程序来托管我们的 .NET 用户控件。在我为帮助我们公司赢得与母公司(拥有遗留 Java 应用程序的公司)的合同而设计的实际示例中,我实际上使用了 JavaBeans。您也可以使用 Java Applet,尽管您只是在为已支持 .NET 用户控件的 Web 页面平台,而额外费力地将 .NET 用户控件嵌入到 Web 页面中。不过,我将这些问题留给用户作为练习。
所以,让我们开发一个简单的基于 Frame 的 Java 应用程序。让我们直接把它放到与 COMTest 项目相同的项目目录中。在这个例子中,位置很重要,因为 VS.NET 或 regasm 没有正确地将类型库与 COM 库关联起来。如果你把你的 .java 文件放在一个单独的地方,或者使用包(我为我们的解决方案使用了包,但为了简单起见我也把它省略了),那么你将负责在稍后使用它们时转换路径。
我继续创建了一个简单的 Java 源文件,其中包含一些本机方法,允许我轻松调整控件大小并更改背景和前景颜色。完成的 Java 源文件可以在 demo project 中找到,但关键代码行如下所示
import java.awt.*;
import java.awt.event.*;
public class JavaTest extends Canvas {
static {
System.loadLibrary("JNITest");
}
// ...
public native void initialize();
public native void destroy();
public native void setCOMSize(int width, int height);
public native void setCOMBackground(long rgb);
public native void setCOMForeground(long rgb);
}
上面标记为 native
的方法是 Java 将从 Java 环境调用到本机环境(在本例中为 C++)的方法。方法 initialize()
和 destroy()
是从 addNotify()
和 removeNotify()
中调用的,Java 在将窗口控件附加到本机资源(例如无处不在的 HWND
)时会调用这些方法。在这些重写中,我们调用 initialize()
和 destroy()
,它们实际上是在我们稍后将讨论的 JNI 包装器中调用的。
维护代码非常简单,所以我不会花太多时间。但是,我会提到我添加了一个 WindowAdapter
(让开发人员不必实现 WindowListener
的每个方法),以便我可以在关闭框架时安全地清理资源。另外,如果没有这段代码,即使您点击窗口框架上的“X”按钮,框架也不会真正关闭!
另一段重要的代码是 ComponentAdapter
(同样,比 ComponentListener
节省了编码),我从中接收调整大小事件。我选择以这种方式实现我的调整大小代码,而不是重写 java.awt.Canvas
(此类扩展了它)中每个重载的 setBounds()
方法。这使得代码更少,但不会在用户调整包含控件的框架大小时调整控件大小。控件仅在用户释放窗口边缘,从而“提交”新的窗口大小时才调整大小。您可以选择任何一种方式——只需确保您调用的方法不是本机方法,而是先在 Java 中执行其操作然后调用本机方法的方法。实际上,在任何情况下都是如此。
所以,唯一剩下的就是编译 Java 类。从包含 JavaTest.java 文件的目录中,运行以下命令
javac JavaTest.java
您将得到三个类文件(两个来自或嵌套声明)。然后我们将从主类文件生成一个 JNI 头文件,供下面我们的 JNI 包装器使用。为此,在同一命令行中键入以下内容
javah -jni -classpath "%CLASSPATH%;." -o JNITest.h JavaTest
这确保了当前目录包含在我们的 CLASSPATH 中,并且我们从之前编译的 JavaTest 类生成了一个名为 JNITest.h 的 JNI 风格头文件。为了简洁起见,我不会在这里列出该头文件,因为它包含大量信息。然而,本质上,您应该会看到一堆与以下模式匹配的方法签名
JNIEXPORT void JNICALL Java_JavaTest_<METHODNAME>
(JNIEnv *, jobject[, additional params]);
如果您查看 JNI 文档,您会发现“Java”始终是函数签名的第一个单词。保持此签名很重要。第二个到第 n-1 个单词是包名成员和类。最后一个单词是方法。至于参数,第一个和第二个总是存在的;第一个代表 Java 环境,第二个代表您本机处理其方法的对象。任何其他参数都取决于您在 Java 源文件中指定的方法中的参数。我们将在下一节讨论如何处理这些参数。
编写包装器:Java 本机接口
在我们深入了解之前,让我先解释一下类型库。类型库(.tlb 文件,或 typelibs)包含有关 COM 对象或在开发过程中有用的对象的信息。这有助于推动使 Visual Studio 如此易于使用和功能强大的 Intellisense 技术!这也有助于预处理器生成头信息,以便 COM 服务器在使用双接口时可以针对正确的函数进行编译和链接。
因此,由于 VS.NET 慷慨地为我们生成了一个类型库,我们现在可以将 Win32 DLL 项目添加到我们的解决方案中,并在一个 COM 服务器中使用该类型库,该 COM 服务器也是我们的 Java 本机接口包装器,即 JNI 包装器。
右键单击解决方案并添加新的 C++ 项目,特别是 Win32 项目。在向导中,单击“应用程序设置”并选择“DLL”以创建动态链接库。单击“完成”,您应该会看到新项目已添加到您的解决方案中。
双击 stdafx.h 并在底部添加以下 #includes
:atlbase.h 和 atlwin.h(按此顺序)。您也可以右键单击项目并添加现有文件,该文件是我们上面用“javah”从 Java 类文件生成的头文件。这只是为了在项目中保留文件引用以便于引用,同时保持其位置。
您还需要修改 VS.NET 的配置并更改您的 VC++ 项目设置
- 在 VS.NET 中点击“工具”->“选项”菜单。
- 找到“项目/VC++ 目录”,并添加“jdk1.4.0\include”目录(取决于您的 JDK 安装目录)。
- 单击“确定”关闭对话框。
- 右键单击您的 VC++ 项目并选择“属性”。
- 在“配置属性/常规”中,将“使用 ATL”设置为“动态链接到 ATL”,并将“字符集”更改为“使用 Unicode 字符集”。JNI 需要这样做,因为 .NET 和 Java 都使用 Unicode 字符串。
- 点击“链接器/输入”,并将“jawt.lib”添加到“附加依赖项”设置中。
- 单击“确定”关闭对话框。
现在,打开您的 JNITest.cpp 文件,并在“stdafx.h”之后,在顶部添加以下 #includes
#include "..\COMTest\JNITest.h" // or wherever it was #include <win32\jawt_md.h>
您还需要添加一个 #import
,这是一个 MS VC++ 扩展,允许您导入和使用类型库。由于它是在预处理期间生成的,因此您必须编译项目才能实际使用它。在上面添加的 #includes
之后包含以下内容
#import "..\COMTest\bin\Release\COMTest.tlb" raw_interfaces_only named_guids using namespace COMTest;
您还会注意到 C++ 使用命名空间。这实际上是某种标准。在使用活动模板库 (ATL) 时,您会经常遇到这种情况。上面使用的命名空间是您在 .NET 类中定义的命名空间,将句点 (.) 替换为下划线 (_)。如果您愿意,可以使用额外的 #import
选项重命名此命名空间(有关更多详细信息,请参阅 MSDN)。
现在继续编译您的 C++ 项目,以便预处理器可以生成一个 .tlh 文件,这是一个使用您在 #import
语句中指定的选项从类型库生成的头文件(有关更多详细信息,请参阅 MSDN)。您可以继续查看 C++ 项目目录的“Release”目录中此头文件包含的内容。它只是一个与其他任何头文件一样的头文件,但您可能会觉得它很有趣。它包含您的 .NET 用户控件项目中公开的每个 COM 对象的所有接口、方法和 GUID。既然您现在拥有 COM 对象的接口声明,让我们添加一些全局变量,这些变量将在以下代码中用到。在您的“using namespace COMTest;
”语句之后,添加以下行
static HWND m_hWnd = NULL; static CAxWindow *m_axWindow = NULL; static CComPtr<icomtest> m_spMyCOMObject = NULL; static OLE_COLOR m_BackColor = NULL; static OLE_COLOR m_ForeColor = NULL; </icomtest>
现在剩下的就是实现我们之前生成的 JNI 头文件中定义的 JNI 方法。编写 JNI 包装器的逻辑起点是通过运行时可调用包装器 (RCW) 实例化 COM 对象的方法。在执行此操作时要记住的一件事是 Java 调用本机方法,因此您不能通过这些本机方法调用 Java 类,也不能在与当前调用相同的线程中的方法主体中调用 Java 库中的方法。因此,您必须创建一个新线程来实际实例化 COM 控件。您的 Java_JavaTest_initialize()
方法应如下所示
JNIEXPORT void JNICALL Java_JavaTest_initialize(JNIEnv *env, jobject canvas) { JAWT awt; JAWT_DrawingSurface *ds; JAWT_DrawingSurfaceInfo *dsi; JAWT_Win32DrawingSurfaceInfo *dsi_win; jboolean result; jint lock; awt.version = JAWT_VERSION_1_3; result = JAWT_GetAWT(env, &awt); assert(result != JNI_FALSE); ds = awt.GetDrawingSurface(env, canvas); assert(ds != NULL); lock = ds->Lock(ds); assert((lock & JAWT_LOCK_ERROR) == 0); dsi = ds->GetDrawingSurfaceInfo(ds); dsi_win = (JAWT_Win32DrawingSurfaceInfo*)dsi->platformInfo; m_hWnd = dsi_win->hwnd; if (m_hWnd != NULL) // Pass control to a new thread _beginthread(initCOMTest, 0, NULL); ds->FreeDrawingSurfaceInfo(dsi); ds->Unlock(ds); awt.FreeDrawingSurface(ds); }
这部分代码几乎从未改变。概念很简单:Windows 中的每个窗口都有一个称为 HWND
的句柄。Java 界面,例如框架和控件,也是如此,只是它们没有直接在 Java 中公开。Java 运行时环境(JRE)确实为每个窗口创建了一个 HWND
,所以您上面所做的只是获取该句柄,将其分配给 JNI 包装器中的全局变量,并将控制权传递给一个新线程,同时当前线程完成当前方法的执行并清理资源。为了使此代码能够编译,您必须声明实际实例化 COM 对象的方法:initCOMTest()
。您应该在 Java_JavaTest_initialize()
之前定义此方法,或者在其之前添加一个前向声明语句。我选择在 Java_JavaTest_initialize()
之前定义该方法,您将在我一起显示所有代码时看到。但是,目前,initCOMTest()
的代码应如下所示
void initCOMTest(void *argv) { if (m_axWindow == NULL) { CoInitialize(NULL); m_axWindow = new CAxWindow(m_hWnd); if (m_axWindow != NULL) { HRESULT hr = S_OK; hr = m_axWindow->CreateControl( CT2OLE(TEXT("COMTest.MyCOMObject")), NULL, NULL); if (SUCCEEDED(hr)) { hr = m_axWindow->QueryControl(IID_IMyCOMObject, (LPVOID*)&m_spMyCOMObject); if (FAILED(hr)) { m_spMyCOMObject = NULL; m_axWindow->DestroyWindow(); return; } if (m_BackColor != NULL) m_spMyCOMObject->put_BackColor(m_BackColor); if (m_ForeColor != NULL) m_spMyCOMObject->put_ForeColor(m_ForeColor); } } } // start the message loop MSG msg; while (GetMessage(&msg, NULL, NULL, NULL)) { TranslateMessage(&msg); DispatchMessage(&msg); } _endthread(); }
我们做的第一件事是用 CoInitialize()
初始化 COM。我们将在 Java_JavaTest_destroy()
的函数定义中卸载 COM。然后我们使用全局 HWND
m_hWnd 创建一个 ATL CAxWindow
类。如果成功,我们使用之前定义的 ProgID 将 .NET COM 对象添加到 CAxWindow
中。如果控件添加成功,我们获取对控件的 CComPtr<IMyCOMObject>
引用,并设置背景和前景颜色。之后,我们启动消息循环(否则控件会立即消失,因为没有消息泵处于活动状态),并在消息循环中断时(当控件被销毁时)发出线程完成信号。当对象被销毁时,我们必须清理其中一些东西,所以我们接下来处理 Java_JavaTest_destroy()
方法
JNIEXPORT void JNICALL Java_JavaTest_destroy(JNIEnv *env, jobject canvas) { if (m_axWindow != NULL) { delete m_axWindow; m_axWindow = NULL; } CoUninitialize(); }
该方法确保 CAxWindow
被正确销毁,并卸载 COM 库和 COM 对象。其余方法都相当直接,因此我不会详细描述每个方法。然而,对于下面列出的其余代码,请记住 .NET 将 System.Drawing.Color
封送为 OLE_COLOR
,它可以从 long
进行类型转换。
完整的代码可以在 demo project 中找到。
运行示例
现在您已经完成了公开 COM 对象的 .NET 组件、Java 应用程序和 JNI 包装器,您已准备好运行示例。如果您有 Java 背景,那您很幸运。如果没有,有些简单的 Java 知识需要学习,这使得它的类加载器很有趣。
如果你指定了一个包(类似于 .NET 中的命名空间),你应该已经创建了一个与之匹配的目录结构,例如 com.codeproject.examples.Class1
将位于 com\codeproject\examples\Class1.java 目录中。当你用“javac”编译它时,你输入 Java 源文件的路径。当你用“java”加载并执行类时,你使用包和类名语法。然而,在这个例子中,为了简单起见,我没有指定包。所以,要运行你的应用程序,请执行以下步骤
- 将 JNITest.dll 从 JNITest 项目的 Release 目录复制到 COMTest 项目目录。
- 打开命令提示符(cmd.exe 或 command.com,具体取决于您的操作系统)。
- 将目录更改为您的 COMTest 项目目录。
- 运行以下命令
java -Djava.library.path="%PATH%;." JavaTest
如果 Java 抱怨找不到类 JavaTest,您的 CLASSPATH 环境变量可能不包含当前目录。永久的解决方案是(根据您的操作系统)更改您的 CLASSPATH 环境变量以包含“.”,这意味着当前目录。临时的解决方案是运行以下命令
java -Djava.library.path="%PATH%;." -cp .;%CLASSPATH% JavaTest
-D 命令行参数定义(或重新定义)一个 Java 环境变量。在本例中,我们将当前目录包含在 PATH 环境变量中,因为当前目录包含我们的 JNITest.dll。由于我们在 Java 应用程序中使用了 System.loadLibrary()
,Java 将尝试从 PATH 加载引用的库(就像 Windows 对可执行文件所做的那样)。如果 JNITest.dll 在我们的 PATH 中,您就不需要包含 -D 命令行参数。如果您不想将库包含在 PATH 中,并且知道它将始终位于特定位置,您可以使用 System.load()
,它接受一个库的路径。考虑到后者所需的维护成本很高,我推荐前一种方法。
您应该会看到一个 Java 窗口弹出,不久之后您的 .NET 用户控件也会出现。填入您的姓名并点击按钮。
摘要
总结一下上述概念,你只需做以下几点
- 创建一个 .NET 用户控件或多个控件,无论是否有依赖项。
- 将一些类、结构和/或枚举公开为具有显式类接口和(可选)事件调度接口的 COM 对象。
- 为编译后的 .NET 用户控件/COM 组件生成并注册类型库(如果您正确设置了项目属性,VS.NET 会为您完成此操作)。
- 编写一个 Java 类,该类具有本机方法,并显式调用函数(这些函数将被封送到您的 COM 组件)本机方法以调整包含控件的大小,以及其他类似方法。
- 从编译后的 Java 类生成 C++ 头文件。
- 在 C++ 类文件中实现 C++ 头文件,通过 ATL(或直接 COM,如果需要)实例化 .NET/COM 组件。
- 编译并运行你的 Java 应用程序。
如果你仔细思考这个解决方案,你会发现它其实并不复杂。COM 弥合了 .NET 和 Java 之间的鸿沟,这两个框架都使用一种与 COM 粘合在一起的原生模块通信的方法。类似的方法也可以用于其他语言和框架。
在一个完美的世界里,我们要么有一种伟大的语言来开发,要么完全独立于用其他语言编写的项目来工作。不幸的是,这不是一个完美的世界,你可能会面临这样的挑战。我希望你上面学到的知识不仅能帮助你在 Java 应用程序中集成丰富的 .NET 用户控件,还能通过示例教你如何将 .NET 与其他语言和框架集成。