精华内容
下载资源
问答
  • 物理数据库-数据库容器-控件数据绑定关系
  • 图中,圆柱体表示容器(上部),矩形表示非容器类(下部)

    图中,圆柱体表示容器(上部),矩形表示非容器类(下部)

    展开全文
  • Android标签容器控件的实现

    千次阅读 2016-07-21 14:57:00
    Android中标签容器控件的实现,在一些APP中我们可以看到一些存放标签的容器控件我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写一个简单的...

    Android中标签容器控件的实现

    介绍

    在一些APP中我们可以看到一些存放标签的容器控件,和我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写一个简单的标签容器控件,项目源码在最后给出

    下面这个是我在手机上截取的一个实例,是在MIUI8系统上截取的

    这里写图片描述

    这个是我实现的效果图

    这里写图片描述

    原理介绍

    根据对整个控件的效果分析,大致可以将控件分别从以下这几个角度进行分析:

    1.首先涉及到自定义的ViewGroup,因为现有的控件没法满足我们的布局效果,就涉及到要重写onMeasure和onLayout,这里需要注意的问题是自定义View的时候,我们需要考虑到View的Padding属性,而在自定义ViewGroup中我们需要在onLayout中考虑Child控件的margin属性否则子类设置这个属性将会失效。整个View的绘制流程是这样的:

    最顶层的ViewRoot执行performTraversals然后分别开始对各个View进行层级的测量、布局、绘制,整个流程是一层一层进行的,也就是说父视图测量时会调用子视图的测量方法,子视图调孙视图方法,一直测量到叶子节点,performTraversals这个函数翻译过来很直白,执行遍历,就说明了这种层级关系。

    2.该控件形式上和ListView的形式比较相近,所以在这里我也模仿ListView的Adapter模式实现了对控件内容的操作,这里对ListView的setAdapter和Adapter的notifyDataSetChanged方法做个简单的解释:

    在ListView调用setAdapter后,ListView会去注册一个Observer对象到这个adapter上,然后当我们在改变设置到adapter上的数据发改变时,我们会调用adapter的notifyDataSetChanged方法,这个方法就会通知所有监听了该Adapter数据改变时的Observer对象,这就是典型的监听者模式,这时由于ListView中的内部成员对象监听了该事件,就可以知道数据源发生了改变,我们需要对真个控件重新进行绘制了,下面来一些相关的源码。

    Adapter的notifyDataSetChanged

    public void notifyDataSetChanged() {
            mDataSetObservable.notifyChanged();
        }

    ListView的setAdapter方法

    @Override
        public void setAdapter(ListAdapter adapter) {
            /**
             *每次设置新的适配的时候,如果现在有的话会做一个解除监听的操作
             */
            if (mAdapter != null && mDataSetObserver != null) {
                mAdapter.unregisterDataSetObserver(mDataSetObserver);
            }
    
            resetList();
            mRecycler.clear();
            /** 省略部分代码.....   */
            if (mAdapter != null) {
                mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
                mOldItemCount = mItemCount;
                mItemCount = mAdapter.getCount();
                checkFocus();
    
                /**
                *在这里对adapter设置了监听,
                *使用的是AdapterDataSetObserver类的对象,该对象定义在ListView的父类AdapterView中
                */
                mDataSetObserver = new AdapterDataSetObserver();
                mAdapter.registerDataSetObserver(mDataSetObserver);
                /** 省略 */
            } else {
                /** 省略 */
            }
    
            requestLayout();
        }

    AdapterView中的内部类AdapterDataSetObserver

    class AdapterDataSetObserver extends DataSetObserver {
    
            private Parcelable mInstanceState = null;
    
            @Override
            public void onChanged() {
                /* ***代码略*** */
                checkFocus();
                requestLayout();
            }
    
            @Override
            public void onInvalidated() {
               /* ***代码略*** */
                checkFocus();
                requestLayout();
            }
    
            public void clearSavedState() {
                mInstanceState = null;
            }
        }

    一段伪代码表示

    ListView{
        Observer observer{
             onChange(){
                  change;
             }
        }
    
        setAdapter(Adapter adapter){
             adapter.register(observer);
        }
    }
    
    Adapter{
        List<Observer> mObservable;
        register(observer){
            mObservable.add(observer);
        }
        notifyDataSetChanged(){
            for(i-->mObserverable.size()){
                mObserverable.get(i).onChange
            }
        }
    }

    实现过程

    获取ViewItem的接口

    package humoursz.gridtag.test.adapter;
    
    import android.view.View;
    
    import java.util.List;
    
    /**
     * Created by zhangzhiquan on 2016/7/19.
     */
    public interface GrideTagBaseAdapter {
        List<View> getViews();
    }
    

    抽象适配器AbsGridTagsAdapter

    package humoursz.gridtag.test.adapter;
    
    import android.database.DataSetObservable;
    import android.database.DataSetObserver;
    
    /**
     * Created by zhangzhiquan on 2016/7/19.
     */
    public abstract class AbsGridTagsAdapter implements GrideTagBaseAdapter {
    
        DataSetObservable mObservable = new DataSetObservable();
    
        public void notification(){
            mObservable.notifyChanged();
        }
        public void registerObserve(DataSetObserver observer){
            mObservable.registerObserver(observer);
        }
        public void unregisterObserve(DataSetObserver observer){
            mObservable.unregisterObserver(observer);
        }
    }
    

    此效果中的需要的适配器,实现了getView接口,主要是模仿了ListView的BaseAdapter

    package humoursz.gridtag.test.adapter;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.widget.TextView;
    
    
    import java.util.ArrayList;
    import java.util.List;
    
    import humoursz.gridtag.test.R;
    import humoursz.gridtag.test.util.UIUtil;
    import humoursz.gridtag.test.widget.GridTagView;
    
    /**
     * Created by zhangzhiquan on 2016/7/19.
     */
    public class MyGridTagAdapter extends AbsGridTagsAdapter {
    
        private Context mContext;
    
        private List<String> mTags;
    
        public MyGridTagAdapter(Context context, List<String> tags) {
            mContext = context;
            mTags = tags;
        }
    
        @Override
        public List<View> getViews() {
            List<View> list = new ArrayList<>();
            for (int i = 0; i < mTags.size(); i++) {
    
                TextView tv = (TextView) LayoutInflater.from(mContext)
                        .inflate(R.layout.grid_tag_item_text, null);
    
                tv.setText(mTags.get(i));
    
                GridTagView.LayoutParams lp = new GridTagView
                        .LayoutParams(GridTagView.LayoutParams.WRAP_CONTENT
                        ,GridTagView.LayoutParams.WRAP_CONTENT);
    
                lp.margin(UIUtil.dp2px(mContext, 5));
    
                tv.setLayoutParams(lp);
    
                list.add(tv);
            }
            return list;
        }
    }

    最后是主角GridTagsView控件

    package humoursz.gridtag.test.widget;
    
    import android.content.Context;
    import android.database.DataSetObserver;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.View;
    import android.view.ViewGroup;
    
    
    import java.util.List;
    
    import humoursz.gridtag.test.adapter.AbsGridTagsAdapter;
    
    /**
     * Created by zhangzhiquan on 2016/7/18.
     */
    public class GridTagView extends ViewGroup {
    
        private int mLines = 1;
    
        private int mWidthSize = 0;
    
        private AbsGridTagsAdapter mAdapter;
    
        private GTObserver mObserver = new GTObserver();
    
        public GridTagView(Context context) {
            this(context, null);
        }
    
        public GridTagView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public GridTagView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public void setAdapter(AbsGridTagsAdapter adapter) {
            if (mAdapter != null) {
                mAdapter.unregisterObserve(mObserver);
            }
            mAdapter = adapter;
            mAdapter.registerObserve(mObserver);
            mAdapter.notification();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int curWidthSize = 0;
            int childHeight = 0;
            mLines = 1;
            for (int i = 0; i < getChildCount(); ++i) {
                View child = getChildAt(i);
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                curWidthSize += getChildRealWidthSize(child);
                if (curWidthSize > widthSize) {
                    /**
                     * 计算一共需要多少行,用于计算控件的高度
                     * 计算方法是,如果当前控件放下后宽度超过
                     * 容器本身的高度,就放到下一行
                     */
                    curWidthSize = getChildRealWidthSize(child);
                    mLines++;
                }
                if (childHeight == 0) {
                    /**
                     * 在第一次计算时拿到字视图的高度作为计算基础
                     */
                    childHeight = getChildRealHeightSize(child);
                }
            }
            mWidthSize = widthSize;
            setMeasuredDimension(widthSize, childHeight == 0 ? heightSize : childHeight * mLines);
    
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (getChildCount() == 0)
                return;
            int childCount = getChildCount();
            LayoutParams lp = getChildLayoutParams(getChildAt(0));
            /**
             * 初始的左边界在自身的padding left和child的margin后
             * 初始的上边界原理相同
             */
            int left = getPaddingLeft() + lp.leftMargin;
            int top = getPaddingTop() + lp.topMargin;
            int curLeft = left;
            for (int i = 0; i < childCount; ++i) {
                View child = getChildAt(i);
    
                int right = curLeft + getChildRealWidthSize(child);
                /**
                 * 计算如果放下当前试图后整个一行到右侧的距离
                 * 如果超过控件宽那就放到下一行,并且左边距还原,上边距等于下一行的开始
                 */
                if (right > mWidthSize) {
                    top += getChildRealHeightSize(child);
                    curLeft = left;
                }
                child.layout(curLeft, top, curLeft + child.getMeasuredWidth(), top + child.getMeasuredHeight());
                /**
                 * 下一个控件的左边开始距离是上一个控件的右边
                 */
                curLeft += getChildRealWidthSize(child);
            }
        }
    
        /**
         * 获取childView实际占用宽度
         * @param child
         * @return 控件实际占用的宽度,需要算上margin否则margin不生效
         */
        private int getChildRealWidthSize(View child) {
            LayoutParams lp = getChildLayoutParams(child);
            int size = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            return size;
        }
    
        /**
         * 获取childView实际占用高度
         * @param child
         * @return 实际占用高度需要考虑上下margin
         */
        private int getChildRealHeightSize(View child) {
            LayoutParams lp = getChildLayoutParams(child);
            int size = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            return size;
        }
    
        /**
         * 获取LayoutParams属性
         * @param child
         * @return
         */
        private LayoutParams getChildLayoutParams(View child) {
            LayoutParams lp;
            if (child.getLayoutParams() instanceof LayoutParams) {
                lp = (LayoutParams) child.getLayoutParams();
            } else {
                lp = (LayoutParams) generateLayoutParams(child.getLayoutParams());
            }
    
            return lp;
        }
    
    
        @Override
        public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attr) {
            return new LayoutParams(getContext(), attr);
        }
    
        @Override
        protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
            return new LayoutParams(p);
        }
    
        public static class LayoutParams extends MarginLayoutParams {
    
            public LayoutParams(Context c, AttributeSet attrs) {
                super(c, attrs);
            }
    
            public LayoutParams(int width, int height) {
                super(width, height);
            }
    
            public LayoutParams(MarginLayoutParams source) {
                super(source);
            }
    
            public LayoutParams(ViewGroup.LayoutParams source) {
                super(source);
            }
    
            public void marginLeft(int left) {
                this.leftMargin = left;
            }
    
            public void marginRight(int r) {
                this.rightMargin = r;
            }
    
            public void marginTop(int t) {
                this.topMargin = t;
            }
    
            public void marginBottom(int b) {
                this.bottomMargin = b;
            }
            public void margin(int m){
                this.leftMargin = m;
                this.rightMargin = m;
                this.topMargin = m;
                this.bottomMargin = m;
            }
        }
    
    
        private class GTObserver extends DataSetObserver {
            @Override
            public void onChanged() {
                removeAllViews();
                List<View> list = mAdapter.getViews();
                for (int i = 0; i < list.size(); i++) {
                    addView(list.get(i));
                }
            }
            @Override
            public void onInvalidated() {
                Log.d("Mrz","fd");
            }
        }
    }
    

    MainActivity

    package humoursz.gridtag.test;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    
    import java.util.List;
    
    import humoursz.gridtag.test.adapter.MyGridTagAdapter;
    import humoursz.gridtag.test.util.ListUtil;
    import humoursz.gridtag.test.widget.GridTagView;
    
    public class MainActivity extends AppCompatActivity {
    
        MyGridTagAdapter adapter;
        GridTagView mGridTag;
        List<String> mList;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mGridTag = (GridTagView)findViewById(R.id.grid_tags);
            mList = ListUtil.getGridTagsList(20);
            adapter = new MyGridTagAdapter(this,mList);
            mGridTag.setAdapter(adapter);
        }
    
        public void onClick(View v){
            mList.removeAll(mList);
            mList.addAll(ListUtil.getGridTagsList(20));
            adapter.notification();
        }
    }
    

    XML 文件

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="humoursz.gridtag.test.MainActivity">
    
        <humoursz.gridtag.test.widget.GridTagView
            android:id="@+id/grid_tags"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        </humoursz.gridtag.test.widget.GridTagView>
        <Button
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="onClick"
            android:text="换一批"/>
    </RelativeLayout>
    

    这样一个简单的控件就写好了,主要需要注意measure和layout否则很多效果都会失效,安卓中的LinearLayout之类的控件实际实现起来要复杂的很多,因为支持的属性实在的太多了,多动手实践可以帮助理解,下面是工程的下载地址

    Android简单标签容器工程源码

    展开全文
  • Android UI控件之容器控件

    千次阅读 2018-10-14 19:30:26
    继承关系: View----ViewGroup---LinearLayout, RelativeLayout, FrameLayout, AbsoluteLayout-----TableLayout(继承自LinearLayout)   LinearLayout 属性有: gravity:设置布局管理器的对齐方式,默认是左...

    基本概念

    继承关系:

    View----ViewGroup---LinearLayout, RelativeLayout, FrameLayout, AbsoluteLayout-----TableLayout(继承自LinearLayout)

     

    LinearLayout

    属性有:

    gravity:设置布局管理器的对齐方式,默认是左对齐和上对齐。

    orientation:设置布局管理器的排列方式,默认是水平排列

    RelativeLayout

    属性:

    gravity, ignoregravity

    子组件的true和false的xml属性

    layout_centerhorizontal,…….

    展开全文
  • ActiveX控件和它的容器

    2011-09-30 13:26:59
    ActiveX控件和它的容器 (转) 1.COM基础 2.ActiveX控件及实现 3.ActiveX控件容器及实现 4.总结 1.COM基础 COM是一种组件开发技术, 它实际上是一种在二进制层上兼容的软件开发方法的...
    ActiveX控件和它的容器 (转) 
    


    1.COM基础

    2.ActiveX控件及实现

    3.ActiveX控件容器及实现

    4.总结


    1.COM基础

    COM是一种组件开发技术, 它实际上是一种在二进制层上兼容的软件开发方法的规范. COM技术是与具体的编程语言无关的技术, 只要是支持COM开发的开发工具都可以用来进行COM应用开发, 而它们在二进制上兼容的要求由各个开发工具来实现, 绝大部分是由编译器实现的.

    COM的基础概念有以下几部分组成,1)接口的定义及实现, 2)IUnknown接口, 3)GUID (COM中所涉及的概念还有很多,具体的可以参阅其他资料 ). 下面分别简单的介绍它们.


    1).接口的定义及实现

    一个接口实际上就是一组定义了具体的功能的函数的集合, 这些定义没有具体的实现. 接口的定义类似于C++中的纯虚类定义, 它定义了接口函数的返回类型、参数个数及函数的功能, COM组件就是靠这些接口相互进行通信. 一个简单的例子如下.(MFC为我们提供了许多方便的宏用来定义接口, 而且在一般情况下, 我们是使用IDL或者是ODL来定义接口, 而不是使用下面这种形式).

    interface IStack:IUnknown {
    virtual void Pop(int* pvalue) = 0;
    virtual void Push(int value) = 0;
    };

    上面定义的就是一个简单的接口IStack. 它定义了两个方法,并且描述了这两个方法的返回类型 (void), 参数个数和类型, 两个函数都用纯虚函数实现,在定义接口的文件里并不需要这两个函数的具体实现. 一般情况下接口的实现可以通过对接口的继承来完成, 但在一个组件实现了多个接口的情况下MFC采用了嵌套子类的实现方法, 具体情况可以参阅其他文档. ]


    2).IUnknown接口

    在上面的例子中, IStack从一个叫做IUnknown的接口继承而来, 那么IUnknown接口是一个什么样的接口呢? 再COM规范中要求, 任何一个COM组件必须实现IUnknown接口, IUnknown接口的主要作用是用来维护COM组件的引用计数和对COM组件实现的接口进行查询, 先让我们看一下IUnknown接口的定义.

    interface IUnknown {
    virtual void QueryInterface(REFIID riid, void** ppvObject) = 0;
    virtual HRESULT AddRef() = 0;
    virtual HRESULT Release() = 0;
    };

    在上面IUnknown接口中, AddRef和Release是用来维护引用计数的. 因为一个COM组件可以同时为多个应用程序服务, 如果没有一种适当的机制来维护COM组件的生存期的话, 那么当一个使用COM组件的应用程序结束时, 这个组件也会被同时释放掉, 那么其他使用这个组件的应用程序就会出现要求访问的组件不存在的错误.所以COM子系统就是用引用计数来解决这个问题, 当一个应用程序要求使用某个组件时, 它就增加这个组件的引用计数, 当这个应用程序结束时, 它就减少这个组件的引用计数, 当一个组件的引用计数为0时, COM子系统就会释放这个组件.

    QueryInterface方法是用来在COM组件中查询一个接口是否被实现的方法, 因为每一个接口都拥有一个能唯一标识它自己的一个ID, 称为IID, 通过传递这IID, 我们就可以查询一个接口是否被该 COM组件实现, 如果该组件实现了该接口,我们就可以利用QueryInterface方法的第二个参数传回的值来使用这个接口的方法.

    3).GUID

    上面提到, 每个接口都由一个唯一标识自己的ID, IID, 同样每个实现了某个接口的C++类也有一个ID, 称为CLSID, 在OLE Automation中, 广泛使用了一种称为类型库的技术, 一个类型库包含了一个COM组件中所有的类型信息, 包括它实现的接口, 枚举类型, 接口的方法, 及接口参数等一些相关的信息, 同样类型库也是用一个表示自己的ID, LIBID. COM子统为了能在众多的COM技术中尽快的找的某个类型的COM组件, 又对COM组件进行了分类管理, 而每个类又有一个类别ID,CATID, 实际上我们可以利用这个CATID来列出系统中的所有的控件(CATID_Control). 上面说的所有这些ID, 实际上是一种类型, GUID. 它们只不过是GUID的不同的typedef.

    GUID是一种利用系统时间和网卡具有的唯一编号的特性生成的一个具有128位的数字. 这个数字在时间和空间上保证了它的唯一性. 所以接口及相关的一些概念都利用GUID来进行区分, 而不是利用它们的名字.

    2.ActiveX控件及实现

    ActiveX控件的最早原型应该是随着VB出现的VBX控件, 由于VBX控件的16位结构并不能适应32位操作系统的要求,于是就诞生了OCX控件, OCX控件是一种32位的自包含的简单应用, 它实际上是一组完成指定的功能函数集合.它实际上是DLL的另外一种表现形式. OCX控件可以有自己的界面,也可以没有界面, 它拥有属性, 方法, 而且一个OCX控件可以触发出某种类型的事件, 用来通知容器它的状态的改变或者是某种外部状态的改变或事件的发生, 实现一个OCX控件必须实现一系列既定的接口, 这使得OCX控件显得有些庞大和冗余, 因为有些控件只需要实现这些接口的一部分, 而且对于Internet 来说, 实现这些多余的接口无疑增加了控件的体积.所以在1996年PDC大会上, 微软提出了它的 Activate Internet的概念, 并把它的一些技术改称为ActiveX技术, ActiveX控件就在原先的OCX控件上经过对要实现的接口的削减而诞生了, 现在只要一个COM组件实现IUnknown接口就可以被称为 ActiveX控件. 所以可以说一个ActiveX控件就是一个实现了IUnknown接口并且支持自注册的简单的 COM组件.

    但是实现一个IUnknown接口的控件显然是没有实际用处的, 所以真正的ActiveX控件还是要实现原先OCX控件定义的一些接口, 用来和它的容器进行交互操作. 下面简要的说明一下一个真正的 ActiveX控件的实现.除了IUnkown接口外, 一个ActiveX控件一般要实现下面接口中的一部分. IOleObject,IOleInPlaceObject,IOleInPlaceActiveObject,IOleControl, IDataObject,IViewObject2, IDispatch, IConnectionPointContainer, ProviderClassInfo[2], ISpecifyPropertyPages, IPerPropertyBrowsing, IPersistStream, IPersistStreamInit,IPersistMemory, IPersistStorage, IPersistMoniker, IPersistPropertyBag,IOleCache[2],IExternalConnection,IRunnableObject, IClassFactory实现要求可以查看MSDN.

    一个ActiveX控件通常具有一些属性和事件.控件的属性一般情况下是通过IDispatch接口实现的.在定义相应的控件属性时, 有一个被称为DISPID的值,这个值是用来被其他使用该控件的容器调用属性时使用的, 因为它们必须通过IDispatch接口的Invoke方法来调用相应的属性.IDispach的方法 Invoke是用来调用响应的属性的关键方法,但是这个方法在调用控件的属性时, 并不是用属性的名字, 而是被称为DISPID的ID值. 在一般情况下, 一个控件通常有它自己的类型库, 容器通过查询控件的类型库得到相应的属性和方法及事件的列表, 并取得它们的DISPID,然后就可以通过Invoke方法来操作它们.

    一个ActiveX控件一般具有三种属性, 固有属性(stock property), 环境属性(ambient property), 自定义属性(custom property). 固有属性是大部分ActiveX控件具有的属性, 比如前景色, 字体等, 环境属性是控件处于容器中时, 有容器提供的一些属性, 如LocaleID, UserMode. 这些属性具有固定的DISPID值, 在控件中可以通过GetAmbientxxxx方法得到这些属性的值. 自定义属性是一个控件要实现自己的某些特定的功能特征时,定义的一些属性, 在容器中这些属性可以通过类型库来得到, 通过对IDispatch接口的调用来处理.

    控件的事件是由控件触发的一个消息或通知, 如果一个控件支持事件, 它必须实现 IConnectionPointContainer和IConnectionPoint接口, 然后控件定义自己的出接口, 这个接口一般是通过用dispinterface声明, 在容器对控件进行事件响应时, 必须使用IDispatch接口的Invoke 方法进行处理, 根据Invoke调用传进来的DISPID我们就可以知道是控件触发了哪一个事件, 根据其他信息, 我们就可以对这个事件进行处理.

    下面简单介绍一下如何利用MFC来进行ActiveX控件的开发. 首先我们使用AppWizard来生成 ActiveX控件的框架, 实际上这个框架已经是一个完整的控件, 在向导的帮助下这个控件已经实现了上面提到的ActiveX控件要实现的接口的一部分重要的接口, 象对事件的基本支持,属性的支持.我们可以在这个框架的帮助下添加我们自己要实现的功能, 为这个控件添加属性方法和事件.VC中的 ClassWizard在这方面提供大量的方便的操作, 在ClassWizard的AcitveX Automation页提供了对 ActiveX控件的属性事件方法的添加.

    对于一个ActiveX控件来说你需要首先弄清楚哪些是要在控件中完成,哪些是要在容器中实现.那么,需要控件完成的你就要考虑用属性或者是方法来实现,而需要容器来完成的你只需将参数通过事件触发传递给容器,在容器端来实现.

    另外,一个比较实际的问题是你的控件将是什么样子.比较简单的方法是在ClassWizard的时候指定控件将继承自那个类,从而拥有该类的外观.但这种方法不够灵活.如果你想定做控件的外观,那么最好的方法还是你自己手绘控件,或者是通过在控件内部添加一些控件形成组合控件.你可以在OnDraw (CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)中来绘制控件.该函数负责控件的绘制,其中pdc是当前系统用的环境设备,rcBounds是当前控件的rect范围,你可以用它来定位.绘制控件还是比较简单的,但前提是你必须要了解Windows的绘图机制.主要是会使用CBrush,CDC,CFont等 MFC的基本绘图类.

    实际上对于ActiveX Control来说,在对它编程完全可以像是对一般的程序一样使用各种MFC的类,但是很多的类将不得不动态的创建,因此你必须掌握好定位.主要是掌握好对各种子类的重绘和刷新的时机和方法.

    关于属性表的创建,属性表允许控件显示它的各种属性,以供察看和编辑.属性表通常以表的对话框的形式实现.你可以在这里改变一些控件的属性.对于大多数的控件来说这已经足够了.

    下面我们来看看VC的AppWinzard Control都为我们做了些什么.用VC的AppWinzard Control你可以快速生成一个ActiveX Control在这里VC自动为我们声称了两个接口:一个用来负责属性和方法. 另一个用来负责事件.这个控件可以在容器运行,但是它什么也不做.并且它的外观也非常简陋.首先让我们来重新绘制它的外观,这在OnDraw中完成.

    // 设置当前的字体,并保留原字体
    CFont* pOldfont;
    pOldfont = SelectFontObject(pdc,m_customfont);

    // 得到当前的各种颜色.其中TranslateColor是为了把OLE_COLOR转换成COLORREF.
    COLORREF textbkcolor = ::GetSysColor(COLOR_BTNFACE);
    COLORREF textforecolor = this->TranslateColor(this->GetForeColor());

    COLORREF edgebkcolor = ::GetSysColor(COLOR_3DFACE);
    COLORREF edgeforecolor = ::GetSysColor(COLOR_3DFACE);

    COLORREF oldbkcolor = pdc->SetBkColor(textbkcolor);
    COLORREF oldforecolor = pdc->SetTextColor(textforecolor);

    if(m_brush.m_hObject = NULL)
    m_brush.CreateSolidBrush(textbkcolor);
    CBrush* pOldbrush = pdc->SelectObject(&m_brush);
    pdc->Rectangle(&rcBounds);
    CSize osize = pdc->GetTextExtent(m_cstrCaption);
    m_size = osize;
    pdc->ExtTextOut((rcBounds.right-osize.cx)/2,
    (rcBounds.bottom-osize.cy)/2,
    ETO_CLIPPED|ETO_OPAQUE,
    rcBounds,
    m_cstrCaption,
    m_cstrCaption.GetLength(),
    NULL);
    UINT borderstyle = EDGE_RAISED;
    UINT borderflags = BF_RECT;

    //画边框
    pdc->SetBkColor(edgebkcolor);
    pdc->SetTextColor(edgeforecolor);
    pdc->DrawEdge((LPRECT)(LPCRECT)rcBounds,borderstyle,borderflags);

    // 恢复设置
    pdc->SetBkColor(oldbkcolor);
    pdc->SetTextColor(oldforecolor);
    pdc->SelectObject(pOldfont);
    pdc->SelectObject(pOldbrush);

    以上代码将为控件绘制一个比较好的外观,当然你可以任意改变直到你满意为止.以后你就可以根据需要来添加一些属性和方法并写出相对的实现.大部分的定义都是由CLASS WINZARD来维护的,所以你可以轻松的添加它们. 一些建议:在控件的属性页的编写过程中,需要将属性页上的标准控件与ActiveX 控件的属性相联系,这样当你在动态的改变标准控件的值时,ActiveX控件的属性会随之改变.但问题是如果你使用别的方法来动态改变属性页上的标准控件的值,则ActiveX控件的属性不会随之改变. 原因很简单,ActiveX控件的属性不知道自己已经发生改变,所以没有接受从标准控件传来的值.这一过程是在DoDataExchange()中的DD_P函数来完成的.由于你手动的改变了标准控件的值,所以你需要使用SetModified()来通知ActiveX控件的属性发生改变,这样DD_P函数就会有效了.另外,在ActiveX控件的属性中的数据类型有一些是OLE_XXX类型,这些类型实际上是一些LONG型的值,并且COLECONTROL 中有一些函数用来转换它们.在类型转换过程中尽量不要使用强制转换,这可能会带来一些意想不到的错误,鼓励使用缓冲区机制.

    关于控件部分其实还有很多东西,可以参阅MFC或其他的文档来了解.

    3.ActiveX控件容器及实现

    ActiveX控件的容器实际上是ActiveX控件的客户端, 它使用ActiveX控件提供的各种功能.但是它也同时为控件提供了一些属性和其他的特征, 使得控件可以更好的和它进行交互和操作. ActiveX 控件的容器实际上是一个OLE容器,然后在实现了相应的接口来支持ActiveX控件后成为ActiveX控件的容器.

    除了IUnknown外,容器程序需要用到下列接口的一部分: IOleInplaceFrame, IOleInPlaceUIWindow, IOleClientSite,IOleInPlaceSite, IAdviseSink, IOleControlSite, IOleControlSite, IDispatch, IProperytNotifySink, IStorage, IOleContainer接口的具体定义请参照MSDN. 在MFC附带的例子中有一个很好的例子, 就是VC中附带的工具ActiveX Control Test Container.

    下面就以这个例子来解释一个ActiveX控件容器的实现及对某些问题的处理. 在这个例子中,使用了VC的向导来生成一个具有Container支持的应用程序, 在生成的类中有一个用来包装每一个嵌入到问档中的OLE对象的类, 一般被称为xxxCntrItem, 在这个例子中被改名CTestContainer98Item. 创建每一个ActiveX控件时都是通过这个类来直接生成,这个类维护了ActiveX控件的一些属性特征.而且这个类支持序列化,

    这样我们就可以通过序列化来保存控件的属性状态等信息. 1).动态创建控件. 这应该是一个ActiveX控件容器最重要的任务. 为了能管理容器中的控件, 首先它必须能动态的创建控件. 因为每一个COM组件都具有一个唯一的ID, CLSID, ActiveX控件也不例外, 但是针对系统中成百上千的COM对象, 我们如何确定哪一个是ActiveX控件呢? 在COM基础中我们提到了为了能更快的定位COM组件并加载它,COM子系统对COM组件实行了分类别管理即利用CATID来分类各种不同的COM组件, ActiveX 控件的CATID是CATID_Control,所以我们可以通过这个信息来找到所有在系统中注册的控件, 一般情况下我们是通过生成一个列表来表示所有这些控件. 下面是经过改写的

    CInsertControlDlg::RefreshControlList()函数

    CArray m_aImplementedCategories;
    CListBox m_lbControls;
    ICatInformationPtr m_pCatInfo;
    CList m_lControls;

    void CInsertControlDlg::RefreshControlList()
    {
    BOOL bDone;
    HRESULT hResult;
    IEnumGUIDPtr pEnum;
    ULONG nImplementCategories;
    CATID* pcatidImpl;
    CLSID clsid;
    LPOLESTR pszName;
    CString strName;
    ULONG iCategory;
    int iItem;
    POSITION posControl;
    CString strServerPath;
    CString strString;
    // 首先,清空列表框,并用m_aImplementCategories的数据填充pcatidImpl, 作为m_pCatInfo函数
    // EnumClassedOfCategories的第二个参数,来获取CLSID的枚举器
    m_lbControls.ResetContent();
    nImplementCategories = m_aImplementCategories.GetSize();
    if (nImplementCategories == 0)
    {
    nImplementCategories = (ULONG)-1;
    pcatidImpl = NULL;
    }
    else
    {
    // 为pcatidImpl分配内存,将m_aImplementCategories数据传给pcatidImpl
    pcatidImpl = (CATID*)_alloca(nImplementCategories * sizeof(CATID));
    for ( iCategory = 0; iCategory < nimplementcategories; iCategory++ )
    pcatidImpl[iCategory]="m_aImplementCategories[iCategory]; // 获取CLSID的枚举器
    hResult = m_pCatInfo->EnumClassesOfCategories(nImplementCategories, pcatidImpl, 0, NULL, &pEnum);
    if (FAILED(hResult)) return;
    //然后通过枚举器枚举所有ActiveX Control的CLSID, 并取得相应的用户类型名称,加入到列表框中.
    bDone = FALSE;
    while (!bDone)
    {
    hResult = pEnum->Next(1, &clsid, NULL); // 获得下一个ActiveX Control的CLSID
    if (hResult == S_OK)
    {
    pszName = NULL;
    hResult = OleRegGetUserType(clsid, USERCLASSTYPE_FULL, &pszName);//得到相应的用户类型名称
    if (SUCCEEDED(hResult))
    {
    strName = pszName;
    CoTaskMemFree(pszName);
    pszName = NULL;
    iItem = m_lbControls.AddString(strName);
    posControl = m_lControls.AddTail(clsid);
    m_lbControls.SetItemDataPtr(iItem, posControl);
    }
    }
    else
    {
    bDone = TRUE;
    }
    }
    OnControlsSelChange();
    }

    上面这个函数演示了如何从众多的COM组件中提取ActiveX控件并把它们添加到一个列表框中,同时保留了它们的CLSID. 其中m_pCatInfo在InitDialog中调用CreateInstance创建自己的实例.

    在我们的到了某个ActiveX控件的CLSID以后, 我们就可以利用CoCreateInstanse函数来生成该控件的实例. 如上面所说的, 每一个ActiveX控件都有一个包装类, 我们在创建控件的时候, 实际上都是通过这个包装类来进行,下面我们看一下, 在这个包装类中创建控件的代码(删除了一些不是很重要的代码).

    BOOL CActiveXContainerCntrItem::CreateControl(REFCLSID clsid)
    {
    IUnknown* pUnknown;
    // 1. 创建控件自己的实例, 在下面的步骤将对控件的一些状态进行初始化
    HRESULT hResult = CoCreateInstance(clsid, NULL, CLSCTX_INPROC|CLSCTX_SERVER,
    IID_IUnknown, (void**)&pUnknown);
    if (FAILED(hResult))
    return FALSE;

    // 2. 在控件中请求IOleObject接口,
    hResult = pUnknown->QueryInterface(IID_IOleObject, (void**)&m_lpObject);
    if (FAILED(hResult))
    {
    pUnknown->Release();
    return FALSE;
    }
    pUnknown->Release();

    CString strUserType;
    GetUserType(USERCLASSTYPE_SHORT, strUserType);
    // 3. 创建一个唯一的名称, 用来维护每个控件实例的唯一性
    GetDocument()->CreateUniqueItemName(this, strUserType, m_strDisplayName);

    // 4. 初始化控件的某些基本信息.
    InitControlInfo();

    BOOL bQuickActivate = FALSE;
    // 5. 如果控件支持IQuickActivate接口, 利用IQuickActive接口激活控件
    bQuickActivate = QuickActivate();
    if (!bQuickActivate)
    {
    // 6. 如果控件不支持IQuickActiveX接口, 通过IOleObject接口设置控件的ClientSite.
    m_lpObject->GetMiscStatus(DVASPECT_CONTENT, &m_dwMiscStatus);
    if (m_dwMiscStatus & OLEMISC_SETCLIENTSITEFIRST)
    hResult = m_lpObject->SetClientSite(GetClientSite());
    if (FAILED(hResult))
    TRACE0("Can't SetClientSite for the Control"n");
    }

    if (SUCCEEDED(hResult))
    {
    // 7. 支持IQuickActivate接口的控件必须使用下面的步骤.
    IPersistStreamInitPtr pPersistStreamInit;
    IPersistStoragePtr pPersistStorage;

    pPersistStreamInit = m_lpObject;
    if (pPersistStreamInit != NULL)
    {
    hResult = pPersistStreamInit->InitNew();
    if (hResult == E_NOTIMPL)
    hResult = S_OK;
    }
    else
    {
    pPersistStorage = m_lpObject;
    if (pPersistStorage != NULL)
    {
    hResult = pPersistStorage->InitNew(m_lpStorage);
    }
    else
    {
    hResult = S_OK;
    }
    }
    }

    return FinishCreate(hResult); // 8. 在此处设置对控件的事件处理和属性处理信息
    }

    下面针对上面注释中提到的一些内容进行说明,

    注释2请求IOleObject接口是用来对后面的一些设置做准备, 因为这个接口要在很多的地方使用, 所以被保存在一个成员变量中.

    注释3是用来区别一个控件的多个实例, 这个方法被文档类实现, 它根据控件的名称和一个数字来维护同一种控件的实例.

    注释4是用来初始化控件的一些基本信息,这些信息是通过读取控件类型库将控件的属性和事件放在各自的列表中, 以后好用来对控件的属性变化和事件进行响应.

    注释5是针对QuickActivate()方法的, QuickActivate()方法首先向控件请求IQuickActivate 接口, 如果控件不支持该接口, 返回FALSE, 如果控件支持该接口, 则初始化两个结构QACONTAINER 和QACONTROL, 然后用这两个结构调用IQuickActivate接口的QuickActivate方法, IQuickActivate接口是为了提高ActiveX控件的加载速度而设计的.在调用了IQuickActivate接口的 QuickActivate()方法之后, IPersist*::Init和IPersist*::InitNew方法必须被调用, 控件应该在QuickActivate方法中建立它的连接点与容器的接收器之间的连接, 如果没有调用 IPersist*::Init和IPersist*::InitNew, 那么这些连接就不会生效.

    到这里控件已经被建立了, 但是这里并没有涉及到与容器有关的内容. 下面讲述具体的容器的实现. 我们在例子中可以看到, 程序的文档类继承自COleDocument, 在COleDocument中文档为我们实现了作为容器所必须实现的一个接口, IOleContainer, 我们在程序中可以通过GetStartPosition (), GetNextItem()等方法来使用这个接口, 这个接口的主要作用是用来遍历容器中的控件或其他的 OLE对象.另外还有一些必须实现的接口实际上已经在MFC中实现, 我们在一般情况下只要简单的使用这些经过封装的函数就可以了, 这里主要讲述一些与控件的属性和事件处理相关的一些问题, 在 ActiveX控件及实现中我们提到, 控件的属性和事件一般是通过IDispatch来实现, 在Test Container中我们可以看到下面的一段用来实现接口映射的代码.

    BEGIN_INTERFACE_MAP( CTestContainer98Item, COleClientItem )
    INTERFACE_PART( CTestContainer98Item, IID_IServiceProvider, ServiceProvider )

    INTERFACE_PART( CTestContainer98Item, IID_IPropertyNotifySink, PropertyNotifySink )
    INTERFACE_PART( CTestContainer98Item, IID_IDispatch, AmbientProperties )
    INTERFACE_PART( CTestContainer98Item, IID_IOleControlSite, OleControlSite )
    // INTERFACE_PART( CTestContainer98Item, IID_IOleInPlaceSiteEx, OleInPlaceSiteWindowless )
    // INTERFACE_PART( CTestContainer98Item, IID_IOleInPlaceSiteWindowless, OleInPlaceSiteWindowless )
    END_INTERFACE_MAP()
     

    我们可以看到, 在上面的接口映射中一共出现了6个接口, 但是有两个入口是被注释的. 下面我们逐一解释这些接口:

    第一个IServiceProvider在这里主要是用来提供IBindHost接口的. 实际上在实现一个容器的时候, 这个接口并不是必须的.

    第二个IPropertyNotifySink是用来实现控件的属性变化通知的接收器. 如果希望你的容器能在其中的控件的属性改变时得到相应的通知, 就要实现这个接口,在这个接口的OnChange方法中你可以得到相应的被改编的属性的DISPID, 有了这个DISPID,你就可以更进一步的控制控件的某些属性特征了.

    第三个接口是用来为控件提供环境属性的. 为控件提供环境属性这个功能是由IDispatch接口实现的, 每一个环境属性都具有特定的DISPID, 所以当控件调用GetAmbientxxx方法时, 控件就会要求容器提供相应的属性的实现,这些属性都是被IDispatch接口实现的.

    第四个接口是IOleControlSite. 这个接口的主要作用是提供一些在容器内部的Site对象对内嵌在其中的控件的管理. 实现这个接口是可选的.

    在上面的接口映射中, 我们并没有看到对控件的事件的处理的接口映射, 在Test Container的代码中我们可以看到下面这段代码.

    BEGIN_INTERFACE_PART( EventHandler, IDispatch )
    STDMETHOD( GetIDsOfNames )( REFIID iid, LPOLESTR* ppszNames, UINT nNames, LCID lcid, DISPID* pDispIDs );
    STDMETHOD( GetTypeInfo )( UINT iTypeInfo, LCID lcid, ITypeInfo** ppTypeInfo );
    STDMETHOD( GetTypeInfoCount )( UINT* pnInfoCount );
    STDMETHOD( Invoke )( DISPID dispidMember, REFIID iid, LCID lcid, WORD wFlags, DISPPARAMS* pdpParams,
    VARIANT* pvarResult, EXCEPINFO* pExceptionInfo, UINT* piArgError );
    END_INTERFACE_PART( EventHandler )
     

    很显然这段代码是用来处理事件的, 但是为什么在接口的映射部分没有它呢? 如果你查看 CTestContainer98Item类的代码时你会发现一个叫做GetInterfaceHook()的方法, 这个方法有一个类型为const void*的参数pv, 这个参数实际上是一个IID类型的指针,看看下面的代码:

    piid = (const IID*)pv;
    if( *piid == m_infoEvents.GetIID() )
    {
    return( &m_xEventHandler );
    }

    现在我们知道了控件的事件是怎么处理的, GetInterfaceHook()方法是CCmdTarget的一个方法, 但是在MSDN中却并没有文档说明.在这个方法中同样也实现了其他几个接口的映射关系.

    到这里我们已经可以了解到要实现一个ActiveX控件的容器所需要实现的接口及相关的一些问题了, MFC的类库为我们做了许多的工作, 它们实现了一些作为控件容器所必须实现的接口, 使我们在开发这类应用程序的时候有了很好的起点.

    4.总结
    上面所谈到的只是一些基本的概念及简单的实现, 开发一个ActiveX控件或者是它的容器都需要很多的知识和技术, 因为COM本身就是一项十分庞大的技术规范, 它涉及了很多方面的知识, 而这些又往往是其它基于COM的技术的基础.
    展开全文
  • 目前可视化快速开发表单有三种容器:一级容器有:表单默认的可视化设计区域Tab面板,他们可以装入:基础面板,Tab面板,明细列表,列表控件四种。其他的控件是无法拖入这种容器的,并且基础面板,Tab面板,明细...
  • 一、容器控件的父子关系 当我们使用DataList、Reapter、GridVieW控件的模板时,经常会在模板中使用Web服务器控件, 那么,浏览该页面时,系统会构建控件树。这时,容器的每行(或项)与控件便形成父子关系。 ...
  • 我们重新来看一下自己定义的Item布局文件,一般这个时候自定义的Item布局文件中都会出现类似于Button、ImageButton、CheckBox等子控件。而这些子控件率先获取了ListView的Item的焦点,使得我们点击ListView的每一项...
  • View ViewGroup之间关系 android中控件的继承关系
  • WinForm界面开发论(二)容器控件

    千次阅读 2007-08-24 14:04:00
    我们的生活中存在着大量的容器关系,比如吃饭需要有碗装米饭,碗放在桌上,桌在家里,家在某一栋楼内,楼在小区内...。一层一层的,控件开发也是这样。在上一篇中,我们介绍了一个简单的控件是如何编写的,本章再...
  • RelativeLayout是相对布局控件:以控件之间相对位置或相对父容器位置进行排列。   相对布局常用属性: 子类控件相对子类控件:值是另外一个控件的id android:layout_above----------位于给定DI控件之上...
  • Android控件的继承关系

    2016-01-23 14:07:53
    Android控件的继承关系
  • 9. Cocos2d-x中基于布局的容器控件 1.1. ScrollView滚动视图 滚动视图是一种常见的容器型控件,它里面可以放置其他组件。Cocos2d-x中的ScrollView本身也是一种Layout,所以也可以在其中使用线形或者相对布局。...
  • winform控件样式的关系

    2013-12-11 00:34:56
    当父控件的颜色为red,子容器的属性会优先由父容器的属性决定。 如果需要改变需要单独设置,还未验证。
  • 父子关系容器控件和镶嵌在其中的控件关系。 【场景】 ASP页面控件定义时,父控件的Visible属性设置为False 【问题】 只要父控件的Visible属性值不改变 在页面的任何生命周期阶段 镶嵌在父控件中的...
  • 《Android自定义控件三部曲文章索引》:http://blog.csdn.net/harvic880925/article/details/50995268 经过上篇的铺垫,这篇就开始正式开始FlowLayout的开发啦,还是先给大家上上效果: 从效果图中可以看到,...
  • PyQt5之设置控件之间的伙伴关系

    千次阅读 2020-05-26 22:04:08
    PyQt5之设置控件之间的伙伴关系 伙伴关系:设置两个控件之间的关联,通过一个控件控制另一个控件。 1、首先选择在Widget Box中拖入一个Frame属性放在窗口中,再往窗口中添加Label、Line Edit属性,并且修改Label...
  • 1、按钮组 2、输入部件组 2.1 QDateTime类 2.2 Qtimer类 3、显示控件组 4、空间间隔组 5、布局管理组 6、容器组 6.1 创建窗口 6.2 使用布局 7、项目视图组
  • 如果父级容器是Canvas,则可以直接设置尺寸。放到其他widget的时候也会保持设定好的尺寸(而不管父容器是什么类型)。 转载于:https://www.cnblogs.com/timy/p/9146243.html...
  • WathOS的导航方式、系统控件使用详解包括Label, Image, Group, Table等
  • 这一此主要记录一下几个很有用的xml布局属性:android:descendantFocusability:该属性是当一个为view获取焦点时,定义viewGroup其子控件两者之间的关系。(例:AdapterView中的item中) 属性的值有三种: ...
  • 文本框类控件

    千次阅读 2017-07-28 09:23:26
    Lable控件常用属性 Text:用来设置或返回标签控件中显示的文本信息; AutoSize:用来获取或设置一个值,该...Anchor:用来确定此控件与其容器控件的固定关系; 所谓容器控件指的是这样一种情况:往往在控件之中还有一
  • Android控件的继承关系

    千次阅读 2016-11-07 21:52:07
    入门级的基础知识,以前对View,VIewGroup,widget,layout等的关系搞不清楚,通过学习后,做了下总结,如下: 1.View,ViewGroup >View: }1、所有高级UI组件都继承View类而实现的 }2、一个View在屏幕上占据一块矩形...
  • Android自定义控件三部曲文章索引

    万次阅读 多人点赞 2016-11-25 17:39:06
    前言:在我从C++转到Android时,就被Android里炫彩斑斓的自定义控件深深折服,想知道如果想利用C++实现这些功能,那是相当困难的。从那时候起,我就想,等我学会了自定义控件,一定要写一篇系列出来,方便后来者能更...
  • MXMLHTML,都是默认按照代码出现的先后顺序来排列控件的层次的,问题是我们一般会在运行的时候动态变换图片或控件的层次的,这样就不能依靠代码出现的先后顺序了,HTML是通过CSS样式的z-index来控制,但是在Flex里...
  • 使用指定其视觉关系和层次结构的容器合并视图。使用称为修饰符的方法来自定义内置视图以及为应用程序创建的视图的显示,行为和交互性。 您将修饰符应用于视图和控件以: 控制视图的大小,位置和外观属性。 响应点击...
  • Java AWT/Swing实现不规则窗体和控件

    千次阅读 多人点赞 2019-05-09 05:27:28
    由于是自学,又是大专,没有科班的基础,所以不是很care算法数据结构,因为Java可以快速作出一个肉眼可以看到的UI,所以我选择了Java而不是C/C++,同时由于MFC这些微软的系统强相关,也就是说,同时放弃了VC++。...
  • 总的看来,Form就像是一个容纳各种控件容器,各种控件都必须直接或者间接的它有依存关系。Form在这里译作“WEB表单”似乎有些不妥。“表单”这个词,在WEB程序员看来,总是HTML里面的“Form”相混淆。“WEB...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,927
精华内容 15,570
关键字:

容器和控件的关系