171.n1-下拉刷新和加载更多

本文介绍了一种基于Android的下拉刷新与加载更多功能的实现方式,详细讲解了通过自定义布局和监听器实现下拉刷新效果,并通过具体代码展示了如何在ListView中实现这一功能。

下拉刷新是利用下拉的时候根据下拉布局的padding来判断当前的状态,然后再加载数据,再隐藏相应的条目。加载更多是在尾部添加一个布局,然后隐藏布局,当滑动到最后一个ListView的时候会拉出来,然后加载数据,加载数据的时候和之前的加载数据不一样,需要在ArrayList中添加上需要加载更多的数据,这是因为如果不添加,初始化的诗句只会是新添加的数据,之前的数据会没有

下面的代码是新修改的,以前的代码没有收起下拉刷新的控件,没有调用onRefreshComplete方法,会有一场。

 /*
     * 获取更多网络数据
     */
    private void getMoreDataFromServer() {
        //从服务器中获取数据
        HttpUtils utils = new HttpUtils();
        utils.send(HttpRequest.HttpMethod.GET, mMoreUrl, new RequestCallBack<String>(){

            @Override
            public void onSuccess(ResponseInfo<String> responseInfo) {
                //获取到返回的结果
                String result = (String) responseInfo.result;
                //System.out.println("tab详情返回结果:" + result);
                System.out.println("tab详情返回结果:====getMoreDataFromServer" );
                parseData(result, true);
                //收起下拉刷新的控件
                lv_news_zixun.onRefreshComplete(true);

            }

            @Override
            public void onFailure(HttpException error, String msg) {
                Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT)
                        .show();
                //打印错误信息
                error.printStackTrace();
                lv_news_zixun.onRefreshComplete(false);

            }

        });

头部的布局文件refresh_header.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <!-- 下拉刷新的布局 -->
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp">

        <ImageView
            android:id="@+id/iv_arr"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/common_listview_headview_red_arrow" />

        <ProgressBar
            android:id="@+id/pb_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:indeterminateDrawable="@drawable/custom_progress"
            android:visibility="invisible" />
    </FrameLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:gravity="center"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="下拉刷新"
            android:textColor="#f00"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="最后刷新:2017-05-10 12:07:07"
            android:textColor="@android:color/darker_gray"
            android:layout_marginTop="5dp"
            android:textSize="16sp" />
    </LinearLayout>

</LinearLayout>

尾部listView的刷新refresh_lsitView_footer.xml

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal" >

    <ProgressBar
        android:id="@+id/pb_pull_list_header"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:indeterminateDrawable="@drawable/custom_progress" />

    <TextView
        android:id="@+id/tv_pull_list_header_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="加载中..."
        android:textColor="#ff0000"
        android:textSize="18sp" />

</LinearLayout>


加载圈使用了自定义的加载圈res/drawable/custom_progress.xml

 

 

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="360" >

    <shape
        android:innerRadius="12dp"
        android:shape="ring"
        android:thickness="3dp"
        android:useLevel="false" >
        <gradient
            android:centerColor="#3f00"
            android:endColor="#f00"
            android:startColor="#fff" />
    </shape>

</rotate>

监听数据加载的业务逻辑TabDetailPager.java

 

package com.ldw.news.base;

import java.util.ArrayList;

import android.app.Activity;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.TextView;
import android.widget.Toast;

import com.google.gson.Gson;
import com.ldw.news.R;
import com.ldw.news.domain.NewsData.NewsTabData;
import com.ldw.news.domain.TabData;
import com.ldw.news.domain.TabData.TabNewsData;
import com.ldw.news.domain.TabData.TopNewsData;
import com.ldw.news.global.ClobalContants;
import com.ldw.news.view.RefreshListView;
import com.ldw.news.view.RefreshListView.OnRefreshListener;
import com.lidroid.xutils.BitmapUtils;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest.HttpMethod;
import com.lidroid.xutils.view.annotation.ViewInject;
import com.viewpagerindicator.CirclePageIndicator;

/*
 * 左边菜单栏右边详情页,最上面对应的tab,页签
 */
public class TabDetailPager extends BaseMenuDetailPager implements OnPageChangeListener{

	//携带数据详情的数据
	private NewsTabData mTabData;
	private TextView text;
	private String mUrl;
	private TabData mTabDetailData;
	@ViewInject(R.id.vp_news)
	private ViewPager mViewPager;
	@ViewInject(R.id.tv_title)
	private TextView tv_title;//banner的标题
	@ViewInject(R.id.indicator)	
	private CirclePageIndicator indicator;//banner下面的小圆点
	@ViewInject(R.id.lv_list)
	private RefreshListView lv_list;//新闻列表
	private ArrayList<TopNewsData> mTopNewsList;//banner的数据
	private ArrayList<TabNewsData> mNewsList;
	private String mMoreUrl;//加载更多的链接地址
	private NewsAdapter mNewsAdapter;
	
	public TabDetailPager(Activity activity, NewsTabData newsTabData) {
		super(activity);
		mTabData = newsTabData;
		//图片的地址
		mUrl = ClobalContants.SERVER_URL + mTabData.url;
	}

	@Override
	public View initView() {
		View view = View.inflate(mActivity, R.layout.tab_detail_pager, null);
		
		//把banner添加到ListView的头部,实现二者的组合
		View headView = View.inflate(mActivity, R.layout.list_header_topnews, null);
		
		ViewUtils.inject(this, view);
		ViewUtils.inject(this, headView);
		//添加到ListView的头部
		lv_list.addHeaderView(headView);
		
		//监听下拉刷新,调用自定义布局中的接口
		lv_list.setOnRefreshListener(new OnRefreshListener(){

			@Override
			public void onRefresh() {	
				//读取数据
				getDataFromServer();
				//结束刷新
				lv_list.onRefreshComplete(true);
				
			}

			@Override
			public void onLoadMore() {
				if(mMoreUrl != null){
					getMoreDataFromServer();
				}else{
					Toast.makeText(mActivity, "最后一条了", Toast.LENGTH_SHORT).show();
					//隐藏脚部局
					lv_list.onRefreshComplete(false);
				}
				
			}
			
		});

		//监听Viewager的滑动
		//mViewPager.setOnPageChangeListener(this);
		
		// 向FrameLayout中动态添加布局
		return view;
	}
	
	@Override
	public void initData(){
		
		getDataFromServer();
		

	}
	
	/*
	 * 获取网络数据
	 */
	private void getDataFromServer() {
		//从服务器中获取数据
		HttpUtils utils = new HttpUtils();
		utils.send(HttpMethod.GET, mUrl, new RequestCallBack<String>(){

			@Override
			public void onSuccess(ResponseInfo<String> responseInfo) {
				//获取到返回的结果
				String result = (String) responseInfo.result;
				System.out.println("tab详情返回结果:" + result);

				parseData(result, false);
				
			}

			@Override
			public void onFailure(HttpException error, String msg) {
				Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT)
				.show();
				//打印错误信息
				error.printStackTrace();
				
			}
			
		});
		
	}
	
	/*
	 * 获取更多网络数据
	 */
	private void getMoreDataFromServer() {
		//从服务器中获取数据
		HttpUtils utils = new HttpUtils();
		utils.send(HttpMethod.GET, mMoreUrl, new RequestCallBack<String>(){

			@Override
			public void onSuccess(ResponseInfo<String> responseInfo) {
				//获取到返回的结果
				String result = (String) responseInfo.result;
				System.out.println("tab详情返回结果:" + result);

				parseData(result, true);
				
			}

			@Override
			public void onFailure(HttpException error, String msg) {
				Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT)
				.show();
				//打印错误信息
				error.printStackTrace();
				
			}
			
		});
		
	}
	
	/*
	 * 解析网络数据,第二个参数表示是否加载下一页
	 */
	protected void parseData(String result,boolean isMore) {
		//使用Gson解析
		Gson gson = new Gson();
		mTabDetailData = gson.fromJson(result, TabData.class);
		System.out.println("解析结果:" + mTabDetailData);
		//服务器读取加载更多数据
		String more = mTabDetailData.data.more;
		if(!TextUtils.isEmpty(more)){
			mMoreUrl = ClobalContants.SERVER_URL + more;
		}else{
			mMoreUrl = null;
		}
		//不加载更多
		if(!isMore){
			//新闻列表的数据ListView
			mNewsList = mTabDetailData.data.news;
			//取到banner新闻top的数据
			mTopNewsList = mTabDetailData.data.topnews;
			
			//top新闻填充
			if(mTopNewsList != null){
				//填充top新闻的pagre
				mViewPager.setAdapter(new TopNewsAdapter());
				
				//banner下面的小圆点,需要在adapter初始化以后才能显示
				indicator.setViewPager(mViewPager);
				indicator.setSnap(true);//支持快照显示
				indicator.setOnPageChangeListener(this);//添加监听,修改文本内容
				indicator.onPageSelected(0);//默认选择第一个,防止图片和远点不对应
				//解析数据的时候初始化标题
				tv_title.setText(mTopNewsList.get(0).title);
			}
			
			//填充新闻列表
			if(mNewsList != null){
				mNewsAdapter = new NewsAdapter();
				lv_list.setAdapter(mNewsAdapter);
			}
		}else{
			//是加载下一页,需要吧数据追加到原来的集合
			ArrayList<TabNewsData> news = mTabDetailData.data.news;
			mNewsList.addAll(news);
			//数据发生改变
			mNewsAdapter.notifyDataSetChanged();
		}
		
	}
	
	/*
	 * 头条新闻,banner
	 */
	class TopNewsAdapter extends PagerAdapter{
		
		private BitmapUtils utils;

		//构造函数
		public TopNewsAdapter(){
			utils = new BitmapUtils(mActivity);
			//加载数据过程中,设置默认加载的图片
			utils.configDefaultLoadingImage(R.drawable.topnews_item_default);
		}

		@Override
		public int getCount() {
			// TODO Auto-generated method stub
			return mTabDetailData.data.topnews.size();
		}

		@Override
		public boolean isViewFromObject(View view, Object object) {
			// TODO Auto-generated method stub
			return view == object;
		}
		
		@Override
		public Object instantiateItem(ViewGroup container, int position) {
			//初始化banner图
			ImageView image = new ImageView(mActivity);
			//填充布局对象
			//image.setImageResource(R.drawable.topnews_item_default);
			//填充平铺布局,基于控件的大小
			image.setScaleType(ScaleType.FIT_XY);
			
			//获取到图片资源的数据的地址
			TopNewsData topNewsData = mTabDetailData.data.topnews.get(position);
			utils.display(image, topNewsData.topimage);
			//添加image数据
			container.addView(image);
			return image;
		}

		@Override
		public void destroyItem(ViewGroup container, int position, Object object) {
			container.removeView((View) object);
		}
		
	}
	
	//新闻列表的适配器
	class NewsAdapter extends BaseAdapter{
		private BitmapUtils utils;

		//构造函数初始化图片
		public NewsAdapter(){
			utils = new BitmapUtils(mActivity);
			//加载的时候显示默认图片
			utils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
		}
		@Override
		public int getCount() {
			// TODO Auto-generated method stub
			return mNewsList.size();
		}

		@Override
		public Object getItem(int position) {
			// TODO Auto-generated method stub
			return mNewsList.get(position);
		}

		@Override
		public long getItemId(int position) {
			// TODO Auto-generated method stub
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder;
			if(convertView == null){
				convertView = View.inflate(mActivity, R.layout.list_news_item, null);
				holder = new ViewHolder();
				holder.iv_pic = (ImageView) convertView.findViewById(R.id.iv_pic);
				holder.tv_date = (TextView) convertView.findViewById(R.id.tv_date);
				holder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
				
				convertView.setTag(holder);
			}else{
				holder = (ViewHolder) convertView.getTag();
			}
			TabNewsData item = (TabNewsData) getItem(position);
			holder.tv_title.setText(item.title);
			holder.tv_date.setText(item.pupdate);
			utils.display(holder.iv_pic, item.listimage);
			return convertView;
		}
		
	}
	
	static class ViewHolder{
		public TextView tv_title;
		public TextView tv_date;
		public ImageView iv_pic;
	}

	@Override
	public void onPageScrolled(int position, float positionOffset,
			int positionOffsetPixels) {
		// TODO Auto-generated method stub
		
	}

	//滑动的监听,当页面被选中的时候更新标题
	@Override
	public void onPageSelected(int position) {
		//TopNewsData topNewsData= mTabDetailData.data.topnews.get(position);
		tv_title.setText(mTopNewsList.get(position).title);
		
	}

	@Override
	public void onPageScrollStateChanged(int state) {
		// TODO Auto-generated method stub
		
	}

}
//监听下拉刷新,调用自定义布局中的接口
		lv_list.setOnRefreshListener(new OnRefreshListener(){

			@Override
			public void onRefresh() {	
				//读取数据
				getDataFromServer();
				//结束刷新
				lv_list.onRefreshComplete(true);
				
			}

			@Override
			public void onLoadMore() {
				if(mMoreUrl != null){
					getMoreDataFromServer();
				}else{
					Toast.makeText(mActivity, "最后一条了", Toast.LENGTH_SHORT).show();
					//隐藏脚部局
					lv_list.onRefreshComplete(false);
				}
				
			}
			
		});

		//监听Viewager的滑动
		//mViewPager.setOnPageChangeListener(this);
		
		// 向FrameLayout中动态添加布局
		return view;
	}
	
	@Override
	public void initData(){
		
		getDataFromServer();
		

	}
	
	/*
	 * 获取网络数据
	 */
	private void getDataFromServer() {
		//从服务器中获取数据
		HttpUtils utils = new HttpUtils();
		utils.send(HttpMethod.GET, mUrl, new RequestCallBack<String>(){

			@Override
			public void onSuccess(ResponseInfo<String> responseInfo) {
				//获取到返回的结果
				String result = (String) responseInfo.result;
				System.out.println("tab详情返回结果:" + result);

				parseData(result, false);
				
			}

			@Override
			public void onFailure(HttpException error, String msg) {
				Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT)
				.show();
				//打印错误信息
				error.printStackTrace();
				
			}
			
		});
		
	}
	
	/*
	 * 获取更多网络数据
	 */
	private void getMoreDataFromServer() {
		//从服务器中获取数据
		HttpUtils utils = new HttpUtils();
		utils.send(HttpMethod.GET, mMoreUrl, new RequestCallBack<String>(){

			@Override
			public void onSuccess(ResponseInfo<String> responseInfo) {
				//获取到返回的结果
				String result = (String) responseInfo.result;
				System.out.println("tab详情返回结果:" + result);

				parseData(result, true);
				
			}

			@Override
			public void onFailure(HttpException error, String msg) {
				Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT)
				.show();
				//打印错误信息
				error.printStackTrace();
				
			}
			
		});
		
	}
	
	/*
	 * 解析网络数据,第二个参数表示是否加载下一页
	 */
	protected void parseData(String result,boolean isMore) {
		//使用Gson解析
		Gson gson = new Gson();
		mTabDetailData = gson.fromJson(result, TabData.class);
		System.out.println("解析结果:" + mTabDetailData);
		//服务器读取加载更多数据
		String more = mTabDetailData.data.more;
		if(!TextUtils.isEmpty(more)){
			mMoreUrl = ClobalContants.SERVER_URL + more;
		}else{
			mMoreUrl = null;
		}
		//不加载更多
		if(!isMore){
			//新闻列表的数据ListView
			mNewsList = mTabDetailData.data.news;
			//取到banner新闻top的数据
			mTopNewsList = mTabDetailData.data.topnews;
			
			//top新闻填充
			if(mTopNewsList != null){
				//填充top新闻的pagre
				mViewPager.setAdapter(new TopNewsAdapter());
				
				//banner下面的小圆点,需要在adapter初始化以后才能显示
				indicator.setViewPager(mViewPager);
				indicator.setSnap(true);//支持快照显示
				indicator.setOnPageChangeListener(this);//添加监听,修改文本内容
				indicator.onPageSelected(0);//默认选择第一个,防止图片和远点不对应
				//解析数据的时候初始化标题
				tv_title.setText(mTopNewsList.get(0).title);
			}
			
			//填充新闻列表
			if(mNewsList != null){
				mNewsAdapter = new NewsAdapter();
				lv_list.setAdapter(mNewsAdapter);
			}
		}else{
			//是加载下一页,需要吧数据追加到原来的集合
			ArrayList<TabNewsData> news = mTabDetailData.data.news;
			mNewsList.addAll(news);
			//数据发生改变
			mNewsAdapter.notifyDataSetChanged();
		}
		
	}
	
	/*
	 * 头条新闻,banner
	 */
	class TopNewsAdapter extends PagerAdapter{
		
		private BitmapUtils utils;

		//构造函数
		public TopNewsAdapter(){
			utils = new BitmapUtils(mActivity);
			//加载数据过程中,设置默认加载的图片
			utils.configDefaultLoadingImage(R.drawable.topnews_item_default);
		}

		@Override
		public int getCount() {
			// TODO Auto-generated method stub
			return mTabDetailData.data.topnews.size();
		}

		@Override
		public boolean isViewFromObject(View view, Object object) {
			// TODO Auto-generated method stub
			return view == object;
		}
		
		@Override
		public Object instantiateItem(ViewGroup container, int position) {
			//初始化banner图
			ImageView image = new ImageView(mActivity);
			//填充布局对象
			//image.setImageResource(R.drawable.topnews_item_default);
			//填充平铺布局,基于控件的大小
			image.setScaleType(ScaleType.FIT_XY);
			
			//获取到图片资源的数据的地址
			TopNewsData topNewsData = mTabDetailData.data.topnews.get(position);
			utils.display(image, topNewsData.topimage);
			//添加image数据
			container.addView(image);
			return image;
		}

		@Override
		public void destroyItem(ViewGroup container, int position, Object object) {
			container.removeView((View) object);
		}
		
	}
	
	//新闻列表的适配器
	class NewsAdapter extends BaseAdapter{
		private BitmapUtils utils;

		//构造函数初始化图片
		public NewsAdapter(){
			utils = new BitmapUtils(mActivity);
			//加载的时候显示默认图片
			utils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
		}
		@Override
		public int getCount() {
			// TODO Auto-generated method stub
			return mNewsList.size();
		}

		@Override
		public Object getItem(int position) {
			// TODO Auto-generated method stub
			return mNewsList.get(position);
		}

		@Override
		public long getItemId(int position) {
			// TODO Auto-generated method stub
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder;
			if(convertView == null){
				convertView = View.inflate(mActivity, R.layout.list_news_item, null);
				holder = new ViewHolder();
				holder.iv_pic = (ImageView) convertView.findViewById(R.id.iv_pic);
				holder.tv_date = (TextView) convertView.findViewById(R.id.tv_date);
				holder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
				
				convertView.setTag(holder);
			}else{
				holder = (ViewHolder) convertView.getTag();
			}
			TabNewsData item = (TabNewsData) getItem(position);
			holder.tv_title.setText(item.title);
			holder.tv_date.setText(item.pupdate);
			utils.display(holder.iv_pic, item.listimage);
			return convertView;
		}
		
	}
	
	static class ViewHolder{
		public TextView tv_title;
		public TextView tv_date;
		public ImageView iv_pic;
	}

	@Override
	public void onPageScrolled(int position, float positionOffset,
			int positionOffsetPixels) {
		// TODO Auto-generated method stub
		
	}

	//滑动的监听,当页面被选中的时候更新标题
	@Override
	public void onPageSelected(int position) {
		//TopNewsData topNewsData= mTabDetailData.data.topnews.get(position);
		tv_title.setText(mTopNewsList.get(position).title);
		
	}

	@Override
	public void onPageScrollStateChanged(int state) {
		// TODO Auto-generated method stub
		
	}

}

自定义的头部布局RefreshListView.java

 

package com.ldw.news.view;

import java.text.SimpleDateFormat;
import java.util.Date;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.ldw.news.R;

/*
 * 详情页新闻列表的下拉刷新,使用自定义的布局来实现
 */
public class RefreshListView extends ListView implements OnScrollListener{
	
	private static final int STATE_PULL_REFRESH = 0;//下拉刷新
	private static final int STATE_RELEASH_REFRESH = 1; //松开刷新
	private static final int STATE_REFRESHING = 2; //刷新
	
	//记录当前的状态,默认下拉刷新
	private int mCurrentState = STATE_PULL_REFRESH;

	//起点的Y坐标
	private int startY = -1;
	private int mHeaderViewHeight;
	private View mHeaderView;
	private TextView tv_title;
	private TextView tv_time;
	private ImageView iv_arr;
	private ProgressBar pb_progress;
	
	private RotateAnimation animUp;
	private RotateAnimation animDown;

	public RefreshListView(Context context) {
		super(context);
		initHeaderView();
		initFooterView();
	}
	
	public RefreshListView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initHeaderView();
		initFooterView();
	}
	
	public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initHeaderView();
		initFooterView();
	}
	
	/*
	 * 下拉刷新布局
	 */
	public void initHeaderView(){
		//填充布局
		mHeaderView = View.inflate(getContext(), R.layout.refresh_header, null);
		//添加头部,先加谁谁在最上面
		this.addHeaderView(mHeaderView);
		
		tv_title = (TextView) mHeaderView.findViewById(R.id.tv_title);
		tv_time = (TextView) mHeaderView.findViewById(R.id.tv_time);
		iv_arr = (ImageView) mHeaderView.findViewById(R.id.iv_arr);
		pb_progress = (ProgressBar) mHeaderView.findViewById(R.id.pb_progress);

		mHeaderView.measure(0, 0);
		//获取到下拉条的高度
		mHeaderViewHeight = mHeaderView.getMeasuredHeight();
		//初始化的时候,隐藏头布局
		mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
		//初始化箭头上下动画
		initArrowAnim();
		
		//初始化时间
		tv_time.setText("最后刷新时间:" + getCurrentTime());
	}
	
	/*
	 * 初始化脚布局
	 */
	private void initFooterView() {
		mFooterView = View.inflate(getContext(),
				R.layout.refresh_listview_footer, null);
		this.addFooterView(mFooterView);

		mFooterView.measure(0, 0);
		mFooterViewHeight = mFooterView.getMeasuredHeight();
		// 隐藏脚布局
		mFooterView.setPadding(0, -mFooterViewHeight, 0, 0);
		//设置滑动监听,根据滑动的状态设置脚标
		this.setOnScrollListener(this);
	}
	
	//触摸事件,触摸可以拉出下拉条
	@Override
	public boolean onTouchEvent(MotionEvent ev){
		switch(ev.getAction()){
		case MotionEvent.ACTION_DOWN:
			//起始点的Y的坐标
			startY = (int) ev.getRawY();
			break;
		case MotionEvent.ACTION_MOVE:
			//如果没有获取到Y的初始值就重新获取
			if(startY == -1){
				startY = (int) ev.getRawY();
			}
			
			// 正在刷新时不做处理
			if (mCurrentState == STATE_REFRESHING) {
				break;
			}
			
			int endY =(int) ev.getRawY();
			int dy = endY - startY;//移动偏移量
			//下拉的时候dy>0,同时是第一个元素,允许下拉
			if(dy > 0 && getFirstVisiblePosition() == 0){
				//计算布局应该显示多少
				int padding = dy - mHeaderViewHeight;
				mHeaderView.setPadding(0, -padding, 0, 0);
				
				//根据padding的正负判断是否完全显示
				if(padding > 0 && mCurrentState != STATE_RELEASH_REFRESH){
					//完全显示,松开刷新
					mCurrentState = STATE_RELEASH_REFRESH;
					refreshState();
				}else if(padding < 0 && mCurrentState != STATE_PULL_REFRESH){
					//没有完全显示padding<0,状态是下拉刷新
					mCurrentState = STATE_PULL_REFRESH;
					refreshState();
				}
				return true;
			}
			
			break;
		case MotionEvent.ACTION_UP:
			//没有触摸以后重置起始点的坐标
			startY = -1;
			//松开以后,判断当前的状态是不是松开刷新,松开刷新就改变状态,如果是下拉刷新就隐藏
			if (mCurrentState == STATE_RELEASH_REFRESH) {
				mCurrentState = STATE_REFRESHING;// 正在刷新
				mHeaderView.setPadding(0, 0, 0, 0);// 显示
				refreshState();
			} else if (mCurrentState == STATE_PULL_REFRESH) {
				mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);// 隐藏
			}
			break;
		}
		return super.onTouchEvent(ev);
		
	}

	/*
	 * 刷新控件的布局
	 */
	private void refreshState() {
		switch(mCurrentState){
		//下拉刷新状态
		case STATE_PULL_REFRESH:
			tv_title.setText("下拉刷新");
			iv_arr.setVisibility(View.VISIBLE);
			pb_progress.setVisibility(View.INVISIBLE);
			iv_arr.startAnimation(animDown);
			break;
		//松开刷新
		case STATE_RELEASH_REFRESH:
			tv_title.setText("松开刷新");
			iv_arr.setVisibility(View.VISIBLE);
			pb_progress.setVisibility(View.INVISIBLE);
			iv_arr.startAnimation(animUp);
			break;
		//正在刷新
		case STATE_REFRESHING:
			tv_title.setText("正在刷新...");
			iv_arr.clearAnimation();// 必须先清除动画,才能隐藏
			iv_arr.setVisibility(View.INVISIBLE);
			pb_progress.setVisibility(View.VISIBLE);
			
			if(mListener != null){
				mListener.onRefresh();
			}
			break;
			
		default:
			break;
		}
		
	}
	
	/**
	 * 初始化箭头动画
	 */
	private void initArrowAnim() {
		// 箭头向上动画
		animUp = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f,
				Animation.RELATIVE_TO_SELF, 0.5f);
		animUp.setDuration(200);
		animUp.setFillAfter(true);

		// 箭头向下动画
		animDown = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF,
				0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
		animDown.setDuration(200);
		animDown.setFillAfter(true);

	}
	
	//创建接口,监听下拉刷新。刷新的时候调用接口方法
	OnRefreshListener mListener;
	private View mFooterView;
	private int mFooterViewHeight;

	public void setOnRefreshListener(OnRefreshListener listener) {
		mListener = listener;
	}

	public interface OnRefreshListener {
		public void onRefresh();

		public void onLoadMore();// 加载下一页数据
	}

	/*
	 * 收起下拉刷新的控件
	 */
	public void onRefreshComplete(boolean success) {
		if (isLoadingMore) {// 正在加载更多...
			mFooterView.setPadding(0, -mFooterViewHeight, 0, 0);
			// 隐藏脚布局
			isLoadingMore = false;
		} else {
			//不加载更多的时候收起控件
			mCurrentState = STATE_PULL_REFRESH;
			tv_title.setText("下拉刷新");
			iv_arr.setVisibility(View.VISIBLE);
			pb_progress.setVisibility(View.INVISIBLE);

			mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);// 隐藏

			if (success) {
				tv_time.setText("最后刷新时间:" + getCurrentTime());
			}
		}
	}
	
	/*
	 * 获取当前时间
	 */
	public String getCurrentTime() {
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		return format.format(new Date());
	}

	//加载更多
	private boolean isLoadingMore;

	//滑动状态的变化
	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		//状态是空闲或者飞速的滑动
		if (scrollState == SCROLL_STATE_IDLE
				|| scrollState == SCROLL_STATE_FLING) {
			// 滑动到最后
			if (getLastVisiblePosition() == getCount() - 1 && !isLoadingMore) {
				System.out.println("到底了.....");
				mFooterView.setPadding(0, 0, 0, 0);// 显示
				setSelection(getCount() - 1);// 改变listview显示位置
				//到最后了显示脚步布局
				isLoadingMore = true;

				if (mListener != null) {
					mListener.onLoadMore();
				}
			}
		}
		
	}

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		// TODO Auto-generated method stub
		
	}




}
	//创建接口,监听下拉刷新。刷新的时候调用接口方法
	OnRefreshListener mListener;
	private View mFooterView;
	private int mFooterViewHeight;

	public void setOnRefreshListener(OnRefreshListener listener) {
		mListener = listener;
	}

	public interface OnRefreshListener {
		public void onRefresh();

		public void onLoadMore();// 加载下一页数据
	}

	/*
	 * 收起下拉刷新的控件
	 */
	public void onRefreshComplete(boolean success) {
		if (isLoadingMore) {// 正在加载更多...
			mFooterView.setPadding(0, -mFooterViewHeight, 0, 0);
			// 隐藏脚布局
			isLoadingMore = false;
		} else {
			//不加载更多的时候收起控件
			mCurrentState = STATE_PULL_REFRESH;
			tv_title.setText("下拉刷新");
			iv_arr.setVisibility(View.VISIBLE);
			pb_progress.setVisibility(View.INVISIBLE);

			mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);// 隐藏

			if (success) {
				tv_time.setText("最后刷新时间:" + getCurrentTime());
			}
		}
	}
	
	/*
	 * 获取当前时间
	 */
	public String getCurrentTime() {
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		return format.format(new Date());
	}

	//加载更多
	private boolean isLoadingMore;

	//滑动状态的变化
	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		//状态是空闲或者飞速的滑动
		if (scrollState == SCROLL_STATE_IDLE
				|| scrollState == SCROLL_STATE_FLING) {
			// 滑动到最后
			if (getLastVisiblePosition() == getCount() - 1 && !isLoadingMore) {
				System.out.println("到底了.....");
				mFooterView.setPadding(0, 0, 0, 0);// 显示
				setSelection(getCount() - 1);// 改变listview显示位置
				//到最后了显示脚步布局
				isLoadingMore = true;

				if (mListener != null) {
					mListener.onLoadMore();
				}
			}
		}
		
	}

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		// TODO Auto-generated method stub
		
	}




}

 

 

 

 

 

在pycharm中使用python语言实行,app.py文件中,我创建了,三个函数,连接MYSQL数据库,执行SQL语句,获取相应数据,sql语句如下:SELECT n1,q1,q2,a1,a2,x1 FROM data.list WHERE n1 ='B207A' AND x1=10,然后将这几个数组传递到line1.html界面中,另一个函数中的SQL如下:SELECT n1,q1,q2,a1,a2,x1 FROM data.list WHERE n1 ='B1991' AND x1=20,传递到line2.html界面中:第三个函数中的SQL如下:SELECT n1,q1,q2,a1,a2,x1 FROM data.list WHERE n1 ='B1991' AND x1=10,传递到line3.html界面中,line1,line2line3界面通过echarts代码创建了一个多重折线图,q1q2a1a2作为多重折线图的折线数据。x1作为X轴数据, 我现在的需求是,在index.html文件中创建三个下拉选项框,第一个下拉选项框的内容是数组n1=B207AB1991,第二个按钮是数组x1的内容:1020第三个选项框的内容是想要将line1line2,line3中的echarts中的 legend: { data: ['q1', 'q2', 'a1', 'a2', ] },涉及到的折线名的内容,第二个选项框可以同时选多个折线数据名称,这样当我选好想要查看那些具体的折线变化可以同时选择多项折线名,然后再点击第四个按钮,确认按钮,通过前三个选择的不同,跳转到不同的line界面,例如在index.html界面中第一个选项框我选择了n1=1991,则只可能执行app.py中函数中 WHERE n1 ='B1991'的函数,再通过第二个选项框,我选择x1=10,则确定下来执行更具体的第三个函数SELECT n1,q1,q2,a1,a2,x1 FROM data.list WHERE n1 ='B1991' AND x1=10,也就是确定跳转到的界面是line3,第三个选择按钮例如我只选择q1q2,那么对应的line3界面中折线数据只有q1q2。给出具体的app.py,index.html,line1.html,line2.html,line3.html界面代码
04-17
// main.cpp - 结构化数据对比工具(Qt4 兼容版 + 线程池支持) // 本程序用于比较两组结构化文本数据(如 CSV 格式)中的对象差异 // 支持 Person Product 两种类型,并使用 QThreadPool 避免界面阻塞 #include // QApplication:管理整个应用程序的核心,包括事件循环、窗口系统等 #include // QWidget:所有 GUI 控件的基类,主窗口继承自它 #include // QVBoxLayout:垂直布局容器,自动将子控件从上到下排列 #include // QHBoxLayout:水平布局容器,自动将子控件从左到右排列 #include // QLabel:显示不可编辑的文字标签,例如说明文字 #include // QTextEdit:多行文本输入/输出框,可用于显示或输入大段文本 #include // QPushButton:可点击的按钮控件,用户通过点击触发操作 #include // QComboBox:下拉选择框,允许用户在多个选项中选择一个 #include // QSplitter:分割窗口区域,用户可以用鼠标拖动调整左右/上下大小 #include // QMessageBox:弹出标准对话框(信息提示、警告、错误等) #include // QFileDialog:打开或保存文件的标准图形界面对话框 #include // QFile:对本地文件进行读写操作的基础类 #include // QTextStream:以流的方式读写文本内容,支持设置编码格式 #include // QDateTime:获取当前时间并格式化为字符串(如 yyyy-MM-dd HH:mm:ss) #include // QClipboard:访问系统剪贴板,实现复制粘贴功能 #include // QThread:提供线程相关功能,用于调试输出当前线程 ID #include // qDebug():打印调试信息到控制台,比 std::cout 更安全跨平台 // 必须包含 QTextCodec 来处理中文字符编码(尤其在 Qt4 中) #include // 多线程相关头文件 #include // QThreadPool:全局线程池,自动管理复用工作线程 #include // QRunnable:表示一个可在子线程中运行的任务接口 #include // (未直接使用)但常配合 QtConcurrent 使用,监听异步任务状态 #include // QObject:信号与槽机制的基础类,也是 invokeMethod 所需 // C++ 标准库头文件 #include // std::shared_ptr:智能指针,自动管理动态分配的对象生命周期 #include // std::map:基于红黑树的键值对容器,支持按 key 快速查找 #include // std::vector:动态数组,用于存储一组对象指针 #include // std::string:C++ 标准字符串类型,用于保存字段内容 #include // std::stringstream:字符串流,用于将一行 CSV 拆分为多个字段 #include // std::atoi, std::atof:C 风格函数,将字符串转为整数或浮点数 #include // std::fabs:浮点数绝对值函数,用于判断两个 double 是否“近似相等” // ==================== 抽象数据对象接口 ==================== // 所有具体的数据结构(如 Person、Product)都必须继承此类 class DataObject { public: // 定义别名 Ptr,简化 shared_ptr 的书写 typedef std::shared_ptr Ptr; // 虚析构函数:确保通过基类指针删除派生类对象时能正确调用其析构函数 virtual ~DataObject() {} // 获取该对象的唯一标识键(例如 ID 或 SKU),用于匹配 A B 中的同一条记录 virtual std::string getKey() const = 0; // 将对象转换为易读的字符串表示形式,用于输出显示 virtual std::string toString() const = 0; // 比较当前对象与另一个对象之间的差异,返回修改字段描述 virtual std::string diff(const DataObject &other) const = 0; // 克隆方法:返回当前对象的一个深拷贝副本(实现多态复制) virtual Ptr clone() const = 0; }; // ==================== 示例结构体 1: Person ==================== // 表示一个人的信息:ID、姓名、年龄 struct Person : DataObject { int id; // 唯一编号 std::string name; // 姓名 int age; // 年龄 // 构造函数:从一行 CSV 文本创建 Person 对象 explicit Person(const std::string &line) : id(0), age(0) { std::stringstream ss(line); // 创建字符串流来逐个提取字段 std::string field; // 临时变量保存当前字段内容 if (std::getline(ss, field, ‘,’)) id = atoi(field.c_str()); // 第一个字段是 id,使用 atoi 转成整数 if (std::getline(ss, field, ‘,’)) name = field; // 第二个字段是 name,直接赋值 if (std::getline(ss, field, ‘,’)) age = atoi(field.c_str()); // 第三个字段是 age,转成整数 } // 默认构造函数:初始化为空对象 Person() : id(0), age(0) {} // 实现 getKey() 方法:返回唯一的键 “Person:ID” std::string getKey() const { return “Person:” + std::to_string(id); } // 实现 toString() 方法:生成可读的对象描述 std::string toString() const { return “Person{id=” + std::to_string(id) + “, name='” + name + “'” + “, age=” + std::to_string(age) + “}”; } // 实现 diff() 方法:比较当前对象与另一个 Person 是否有不同字段 std::string diff(const DataObject &other) const { const Person& p = static_cast<const Person&>(other); // 安全类型转换(已知类型) std::string changes; // 存储变化信息 if (name != p.name) { changes += " name: ‘" + name + "’ -> ‘" + p.name + "’;"; } if (age != p.age) { changes += " age: " + std::to_string(age) + " -> " + std::to_string(p.age) + “;”; } // 如果没有任何变化,返回“(无变化)”提示 return changes.empty() ? “(无变化)” : changes; } // 实现 clone() 方法:返回一个新的 Person 副本(深拷贝) Ptr clone() const { return std::make_shared(*this); // 使用智能指针包装新对象 } }; // ==================== 示例结构体 2: Product ==================== // 表示商品信息:SKU 编号、标题、价格 struct Product : DataObject { std::string sku; // 商品唯一编号 std::string title; // 商品名称 double price; // 商品价格(浮点数) // 构造函数:从一行 CSV 文本创建 Product 对象 explicit Product(const std::string &line) : price(0.0) { std::stringstream ss(line); std::string field; if (std::getline(ss, field, ‘,’)) sku = field; // 第一列:SKU if (std::getline(ss, field, ‘,’)) title = field; // 第二列:title if (std::getline(ss, field, ‘,’)) price = atof(field.c_str()); // 第三列:price,使用 atof 转换 } // 默认构造函数 Product() : price(0.0) {} // 返回唯一键:“Product:SKU” std::string getKey() const { return “Product:” + sku; } // 易读格式输出(价格保留两位小数) std::string toString() const { char buf[100]; snprintf(buf, sizeof(buf), “%.2f”, price); // 格式化价格 return “Product{sku='” + sku + “‘, title=’” + title + “', price=” + std::string(buf) + “}”; } // 比较两个 Product 是否有字段不同 std::string diff(const DataObject &other) const { const Product& p = static_cast<const Product&>(other); std::string changes; if (title != p.title) { changes += " title: ‘" + title + "’ -> ‘" + p.title + "’;"; } if (fabs(price - p.price) > 1e-5) { // 浮点数不能直接 == 比较,需用误差判断 char oldPrice[20], newPrice[20]; snprintf(oldPrice, sizeof(oldPrice), “%.2f”, price); snprintf(newPrice, sizeof(newPrice), “%.2f”, p.price); changes += " price: " + std::string(oldPrice) + " -> " + std::string(newPrice) + “;”; } return changes.empty() ? “(无变化)” : changes; } // 返回克隆对象(深拷贝) Ptr clone() const { return std::make_shared(*this); } }; // ==================== 数据解析器工厂 ==================== // 静态类:根据用户选择的类型将文本解析为对象列表 class DataParser { public: // 定义别名:一组数据对象的列表 typedef std::vectorDataObject::Ptr ObjectList; // 静态方法:将输入文本按指定类型解析成对象列表 static ObjectList parse(const QString &text, const QString &type) { ObjectList result; // 存放成功解析的对象 QStringList lines = text.split(‘\n’, QString::SkipEmptyParts); // 按换行拆分,跳过空行 // 遍历每一行文本 for (int i = 0; i < lines.size(); ++i) { const QString &line = lines[i]; // 当前行 try { if (type == “Person”) { // 解析为 Person 类型并加入结果集 result.push_back(std::make_shared(line.toStdString())); } else if (type == “Product”) { // 解析为 Product 类型 result.push_back(std::make_shared(line.toStdString())); } else { // 不支持的类型,打印调试日志 qWarning(“未知类型: %s”, type.toLocal8Bit().data()); } } catch (…) { // 捕获任何异常(比如数字转换失败) // 注意:这里不能弹 QMessageBox(非主线程禁止操作 GUI) // 我们改为记录错误并在主线程提示 qDebug() << “[Worker] 解析失败行:” << line; } } return result; // 返回解析完成的对象列表 } }; // ==================== 异步任务类:继承 QRunnable ==================== // 表示一个异步加载并解析文件的任务 class LoadDataTask : public QRunnable { public: // 枚举标识是哪个输入源(A 左侧 / B 右侧) enum Source { SourceA, SourceB }; // 构造函数:传入文件路径、数据类型、来源(A 或 B)、接收结果的对象 LoadDataTask(const QString &filePath, const QString &dataType, Source src, QObject *receiver) : m_filePath(filePath), m_dataType(dataType), m_source(src), m_receiver(receiver) {} // run() 是 QRunnable 的纯虚函数,会被线程池自动调用,在子线程中执行 void run() override { // 输出调试信息:正在处理哪个文件、运行在线程几 qDebug() << “正在子线程中处理文件:” << m_filePath << " | 线程ID:" << QThread::currentThreadId(); // 1. 读取文件内容 QFile file(m_filePath); // 创建文件对象 QString content; // 用于存储读取的全部文本 bool success = false; // 记录是否成功 QString error; // 存储错误信息(如果失败) // 尝试以只读文本模式打开文件 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&file); // 创建文本流 stream.setCodec(QTextCodec::codecForName(“UTF-8”)); // 设置编码为 UTF-8(支持中文) content = stream.readAll(); // 一次性读取所有内容 file.close(); // 关闭文件 success = true; // 标记成功 } else { error = “无法打开文件”; // 设置错误消息 } // 2. 如果读取成功,则进行解析(也在子线程) std::vectorDataObject::Ptr parsedData; if (success) { parsedData = DataParser::parse(content, m_dataType); // 调用解析器解析文本 } // 3. 准备结果对象,封装所有需要传回主线程的数据 AsyncTaskResult result; result.success = success; // 是否成功 result.content = content; // 文件原始内容(用于显示在 QTextEdit) result.parsedData = parsedData; // 解析后的对象列表(用于后续对比) result.source = m_source; // 来源是 A 还是 B result.errorMsg = error; // 错误信息(如果有) result.filePath = m_filePath; // 文件路径(用于错误提示) // 4. 使用元对象系统跨线程发送信号通知主线程 // Qt::QueuedConnection 确保槽函数在目标线程(GUI 主线程)排队执行 QMetaObject::invokeMethod(m_receiver, “onTaskFinished”, Qt::QueuedConnection, Q_ARG(AsyncTaskResult, result)); } private: QString m_filePath; // 要加载的文件路径 QString m_dataType; // 当前选择的数据类型(“Person” 或 “Product”) Source m_source; // 来源标识(A 或 B) QObject *m_receiver; // 接收结果的对象(即主窗口 this) // 内部结构体:封装任务执行结果,便于跨线程传递 struct AsyncTaskResult { bool success; // 成功标志 QString content; // 文件内容(QString) std::vectorDataObject::Ptr parsedData; // 解析后的对象列表 Source source; // 来源 A/B QString errorMsg; // 错误信息 QString filePath; // 文件路径 }; // 声明该结构体支持 Qt 元对象系统序列化(让 invokeMethod 能传递它) Q_DECLARE_METATYPE(AsyncTaskResult) }; // 手动注册类型到 Qt 元对象系统(必须放在类外) Q_DECLARE_METATYPE(LoadDataTask::AsyncTaskResult) // ==================== 主窗口类声明 ==================== // 继承自 QWidget,作为主界面窗口 class StructuredDataComparator : public QWidget { Q_OBJECT // 必须添加:启用 Qt 的元对象系统(信号槽、moc 工具需要) public: // 构造函数:parent 表示父窗口,默认为空 explicit StructuredDataComparator(QWidget *parent = 0); private slots: // 槽函数:响应按钮点击事件 void onCompare(); // 开始对比 A B 的数据 void onClear(); // 清空所有输入输出内容 void onCopyResult(); // 复制对比结果到系统剪贴板 void onLoadFileA(); // 加载文件 A 到左侧输入框 void onLoadFileB(); // 加载文件 B 到右侧输入框 void onTaskFinished(const LoadDataTask::AsyncTaskResult &result); // 接收子线程完成的结果 private: // 私有方法:初始化用户界面布局控件 void setupUI(); // 成员变量:GUI 控件指针 QTextEdit *inputA; // 左侧输入框(旧数据) QTextEdit *inputB; // 右侧输入框(新数据) QTextEdit *outputArea; // 下方输出区域(显示对比结果) QPushButton *compareBtn; // “开始对比”按钮 QPushButton *clearBtn; // “清空”按钮 QPushButton *copyBtn; // “复制结果”按钮 QComboBox *dataTypeCombo; // 下拉框选择数据类型(Person / Product) // 缓存最后一次加载成功的解析数据(用于对比,避免重复解析) std::vectorDataObject::Ptr m_lastParsedA; std::vectorDataObject::Ptr m_lastParsedB; }; // ========== 主窗口构造函数实现 ========== StructuredDataComparator::StructuredDataComparator(QWidget *parent) : QWidget(parent) // 调用父类 QWidget 的构造函数 { // 注册自定义类型,使它可以跨线程传递(由 QMetaObject::invokeMethod 使用) qRegisterMetaTypeLoadDataTask::AsyncTaskResult(“LoadDataTask::AsyncTaskResult”); setupUI(); // 初始化界面布局控件 } // ========== setupUI 函数实现:构建整个用户界面 ========== void StructuredDataComparator::setupUI() { setWindowTitle(“🧾 结构化数据对比工具(多线程安全)”); // 设置窗口标题 resize(900, 700); // 设置初始窗口大小(宽900px,高700px) QVBoxLayout *mainLayout = new QVBoxLayout(this); // 创建主垂直布局,作为顶层容器 // — 第一部分:顶部控件 —— 选择数据类型 — QHBoxLayout *topLayout = new QHBoxLayout; // 水平布局放置标签下拉框 topLayout->addWidget(new QLabel(“数据类型:”)); // 添加说明文字 dataTypeCombo = new QComboBox; // 创建下拉选择框 dataTypeCombo->addItem(“Person”); // 添加选项:Person dataTypeCombo->addItem(“Product”); // 添加选项:Product topLayout->addWidget(dataTypeCombo); // 将下拉框加入布局 topLayout->addStretch(); // 右侧留白,使控件靠左排列 mainLayout->addLayout(topLayout); // 将顶部布局加入主布局 // — 第二部分:左右输入区(使用分割器)— QSplitter *splitter = new QSplitter(Qt::Horizontal); // 创建水平分割器,左右可拖动 // 左侧容器:旧数据输入 QWidget *containerA = new QWidget; // 包裹左侧控件的容器 QVBoxLayout *layoutA = new QVBoxLayout; // 垂直布局 layoutA->addWidget(new QLabel(“来源 A(旧数据)”)); // 添加标签说明 inputA = new QTextEdit; // 创建多行文本框 inputA->setPlaceholderText(“输入格式:\nid,name,age\n1,Alice,30”); // 占位提示文本 layoutA->addWidget(inputA); // 将文本框加入布局 containerA->setLayout(layoutA); // 容器应用此布局 // 右侧容器:新数据输入 QWidget *containerB = new QWidget; QVBoxLayout *layoutB = new QVBoxLayout; layoutB->addWidget(new QLabel(“来源 B(新数据)”)); inputB = new QTextEdit; inputB->setPlaceholderText(“输入格式:\nid,name,age\n2,Bob,25”); layoutB->addWidget(inputB); containerB->setLayout(layoutB); // 将左右两个容器加入分割器 splitter->addWidget(containerA); splitter->addWidget(containerB); mainLayout->addWidget(splitter); // 将分割器加入主布局 // — 第三部分:按钮行 — QHBoxLayout *btnLayout = new QHBoxLayout; // 水平布局放置按钮 QPushButton *loadBtnA = new QPushButton(“📁 加载 A”); // 加载文件 A 的按钮 QPushButton *loadBtnB = new QPushButton(“📁 加载 B”); // 加载文件 B 的按钮 compareBtn = new QPushButton(“🔍 开始对比”); // 开始对比按钮 clearBtn = new QPushButton(“🗑️ 清空”); // 清空按钮 copyBtn = new QPushButton(“📋 复制结果”); // 复制结果按钮 // 按顺序添加按钮到布局 btnLayout->addWidget(loadBtnA); btnLayout->addWidget(loadBtnB); btnLayout->addSpacing(40); // 插入空白间距,美观分隔 btnLayout->addWidget(compareBtn); btnLayout->addWidget(clearBtn); btnLayout->addWidget(copyBtn); mainLayout->addLayout(btnLayout); // 将按钮行加入主布局 // — 第四部分:输出结果显示区 — outputArea = new QTextEdit; // 创建输出文本框 outputArea->setReadOnly(true); // 设置只读,防止用户误改 outputArea->setPlaceholderText(“对比结果将显示在这里…”); // 提示语 mainLayout->addWidget(outputArea); // 加入主布局 // === 最后:连接信号与槽(使用 Qt4 宏语法)=== connect(compareBtn, SIGNAL(clicked()), this, SLOT(onCompare())); // 点击“开始对比”执行 onCompare connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear())); // 点击“清空”执行 onClear connect(copyBtn, SIGNAL(clicked()), this, SLOT(onCopyResult())); // 点击“复制”执行 onCopyResult connect(loadBtnA, SIGNAL(clicked()), this, SLOT(onLoadFileA())); // 点击“加载 A”执行 onLoadFileA connect(loadBtnB, SIGNAL(clicked()), this, SLOT(onLoadFileB())); // 点击“加载 B”执行 onLoadFileB } // ========== 槽函数:执行数据对比逻辑 ========== void StructuredDataComparator::onCompare() { QString type = dataTypeCombo->currentText(); // 获取当前选择的数据类型 QString textA = inputA->toPlainText(); // 获取左侧输入的所有文本 QString textB = inputB->toPlainText(); // 获取右侧输入的所有文本 // 检查是否至少有一个输入为空 if (textA.trimmed().isEmpty() || textB.trimmed().isEmpty()) { outputArea->setText(“⚠️ 请确保两个输入框都有数据!”); // 提示用户 return; // 提前退出 } // 使用已缓存的 parsed 数据(由线程任务填充) std::vectorDataObject::Ptr listA = m_lastParsedA; std::vectorDataObject::Ptr listB = m_lastParsedB; // 如果没有缓存且文本不为空,则临时同步解析一次(适用于手动粘贴的小数据) if (listA.empty() && !textA.isEmpty()) { listA = DataParser::parse(textA, type); } if (listB.empty() && !textB.isEmpty()) { listB = DataParser::parse(textB, type); } // 创建 map:key -> object,便于 O(log n) 查找 std::map<std::string, DataObject::Ptr> mapA, mapB; for (size_t i = 0; i < listA.size(); ++i) mapA[listA[i]->getKey()] = listA[i]; for (size_t i = 0; i < listB.size(); ++i) mapB[listB[i]->getKey()] = listB[i]; // 定义三个向量分别存储新增、删除、修改的对象描述 std::vectorstd::string added; std::vectorstd::string removed; std::vectorstd::string modified; // 在 A 中但不在 B 中 → 被删除 for (std::map<std::string, DataObject::Ptr>::iterator it = mapA.begin(); it != mapA.end(); ++it) { if (mapB.find(it->first) == mapB.end()) { removed.push_back(it->second->toString()); } } // 在 B 中但不在 A 中 → 新增 for (std::map<std::string, DataObject::Ptr>::iterator it = mapB.begin(); it != mapB.end(); ++it) { if (mapA.find(it->first) == mapA.end()) { added.push_back(it->second->toString()); } } // 同时存在于 A B → 检查是否修改 for (std::map<std::string, DataObject::Ptr>::iterator it = mapB.begin(); it != mapB.end(); ++it) { std::string key = it->first; std::map<std::string, DataObject::Ptr>::iterator oldIt = mapA.find(key); if (oldIt != mapA.end()) { std::string diffStr = it->second->diff(*oldIt->second); if (diffStr != “(无变化)”) { modified.push_back(“🔹 " + key + “\n 修改: " + diffStr); } } } // 生成最终报告文本 QString result; result += “🧾 结构化数据对比报告\n”; result += “==============================\n”; result += QString(“📅 时间: %1\n”).arg(QDateTime::currentDateTime().toString(“yyyy-MM-dd HH:mm:ss”)); result += QString(“📊 类型: %1\n\n”).arg(type); result += QString(“🆕 新增 (%1):\n”).arg(added.size()); for (size_t i = 0; i < added.size(); ++i) { result += " • " + QString::fromStdString(added[i]) + “\n”; } result += “\n”; result += QString(“🗑️ 删除 (%1):\n”).arg(removed.size()); for (size_t i = 0; i < removed.size(); ++i) { result += " • " + QString::fromStdString(removed[i]) + “\n”; } result += “\n”; result += QString(”✏️ 修改 (%1):\n”).arg(modified.size()); for (size_t i = 0; i < modified.size(); ++i) { result += QString::fromStdString(modified[i]) + “\n”; } // 显示在输出框 outputArea->setText(result); } // ========== 清空所有内容 ========== void StructuredDataComparator::onClear() { inputA->clear(); // 清空左侧输入框 inputB->clear(); // 清空右侧输入框 outputArea->clear(); // 清空输出框 m_lastParsedA.clear(); // 清除缓存的解析结果 m_lastParsedB.clear(); } // ========== 复制结果到剪贴板 ========== void StructuredDataComparator::onCopyResult() { // 获取 QApplication 的全局剪贴板对象,并设置当前输出文本 QApplication::clipboard()->setText(outputArea->toPlainText()); // 弹出成功提示对话框 QMessageBox::information(this, “已复制”, “结果已复制到剪贴板!”); } // ========== 加载文件 A(启动线程任务)========== void StructuredDataComparator::onLoadFileA() { // 弹出文件选择对话框,过滤 .csv .txt 文件 QString filePath = QFileDialog::getOpenFileName(this, “加载 A 文件”, “”, “CSV/TXT (*.csv *.txt)”); if (!filePath.isEmpty()) { // 用户未取消选择 // 创建任务:传入文件路径、当前类型、来源 A、接收者(this) LoadDataTask *task = new LoadDataTask(filePath, dataTypeCombo->currentText(), LoadDataTask::SourceA, this); task->setAutoDelete(true); // 任务完成后由线程池自动 delete,避免内存泄漏 // 提交到全局线程池执行(run() 将在后台线程被调用) QThreadPool::globalInstance()->start(task); // 显示加载中提示(用户体验优化) inputA->setPlainText(“⏳ 正在加载并解析文件…”); } } // ========== 加载文件 B ========== void StructuredDataComparator::onLoadFileB() { QString filePath = QFileDialog::getOpenFileName(this, “加载 B 文件”, “”, “CSV/TXT (*.csv *.txt)”); if (!filePath.isEmpty()) { // 创建任务:来源为 B LoadDataTask *task = new LoadDataTask(filePath, dataTypeCombo->currentText(), LoadDataTask::SourceB, this); task->setAutoDelete(true); QThreadPool::globalInstance()->start(task); // 显示加载中提示 inputB->setPlainText(“⏳ 正在加载并解析文件…”); } } // ========== 接收子线程完成的结果(主线程执行)========== void StructuredDataComparator::onTaskFinished(const LoadDataTask::AsyncTaskResult &result) { // 此函数在主线程执行,可以安全地更新 UI qDebug() << “[MainThread] 收到任务完成通知,来源:” << (result.source == LoadDataTask::SourceA ? “A” : “B”) << " | 线程ID:" << QThread::currentThreadId(); if (result.success) { // 更新对应的输入框文本 if (result.source == LoadDataTask::SourceA) { inputA->setPlainText(result.content); // 显示文件内容 m_lastParsedA = result.parsedData; // 缓存解析结果供对比使用 } else { inputB->setPlainText(result.content); m_lastParsedB = result.parsedData; } // 提示用户加载成功 QMessageBox::information(this, “完成”, “文件加载并解析成功!”); } else { // 加载失败则显示错误信息 QString msg = “加载失败:” + result.errorMsg + “\n文件:” + result.filePath; if (result.source == LoadDataTask::SourceA) { inputA->setPlainText(msg); } else { inputB->setPlainText(msg); } // 弹出错误对话框 QMessageBox::critical(this, “错误”, msg); } } // ========== MOC 编译支持(必须)========== // Qt 的元对象编译器(moc)需要处理含有 Q_OBJECT 的类 // 此行告诉 moc 处理这个文件中的信号槽 #include “main.moc” // ========== 程序入口点 ========== int main(int argc, char *argv[]) { QApplication app(argc, argv); // 创建 Qt 应用对象,管理事件循环 #if QT_VERSION < 0x050000 // Qt4 推荐:设置默认字符串编码为 UTF-8,以便正确显示中文 QTextCodec::setCodecForCStrings(QTextCodec::codecForName(“UTF-8”)); #endif StructuredDataComparator tool; // 创建主窗口实例 tool.show(); // 显示窗口 return app.exec(); // 启动事件循环,等待用户交互 } 这段代码在加入了多线程处理之后,可能是将代码进行头文件源文件分离时出现了问题,请帮我将这段代码按头文件源文件的方式进行分离,并说明引用关系,同时使用QTableView来显示数据,对ui界面重新进行设计
最新发布
09-26
.m-md-n1 { margin: -0.25rem !important; } .m-md-n2 { margin: -0.5rem !important; } .m-md-n3 { margin: -1rem !important; } .m-md-n4 { margin: -1.5rem !important; } .m-md-n5 { margin: -3rem !important; } .m-md-n6 { margin: -5rem !important; } .mx-md-n1 { margin-right: -0.25rem !important; margin-left: -0.25rem !important; } .mx-md-n2 { margin-right: -0.5rem !important; margin-left: -0.5rem !important; } .mx-md-n3 { margin-right: -1rem !important; margin-left: -1rem !important; } .mx-md-n4 { margin-right: -1.5rem !important; margin-left: -1.5rem !important; } .mx-md-n5 { margin-right: -3rem !important; margin-left: -3rem !important; } .mx-md-n6 { margin-right: -5rem !important; margin-left: -5rem !important; } .my-md-n1 { margin-top: -0.25rem !important; margin-bottom: -0.25rem !important; } .my-md-n2 { margin-top: -0.5rem !important; margin-bottom: -0.5rem !important; } .my-md-n3 { margin-top: -1rem !important; margin-bottom: -1rem !important; } .my-md-n4 { margin-top: -1.5rem !important; margin-bottom: -1.5rem !important; } .my-md-n5 { margin-top: -3rem !important; margin-bottom: -3rem !important; } .my-md-n6 { margin-top: -5rem !important; margin-bottom: -5rem !important; } .mt-md-n1 { margin-top: -0.25rem !important; } .mt-md-n2 { margin-top: -0.5rem !important; } .mt-md-n3 { margin-top: -1rem !important; } .mt-md-n4 { margin-top: -1.5rem !important; } .mt-md-n5 { margin-top: -3rem !important; } .mt-md-n6 { margin-top: -5rem !important; } .me-md-n1 { margin-right: -0.25rem !important; } .me-md-n2 { margin-right: -0.5rem !important; } .me-md-n3 { margin-right: -1rem !important; } .me-md-n4 { margin-right: -1.5rem !important; } .me-md-n5 { margin-right: -3rem !important; } .me-md-n6 { mar
04-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值